{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8c176ef6-e41c-48da-bfa4-76217614bbbc",
   "metadata": {},
   "source": [
    "# Image to Image search Using OpenAI's Open source CLIP Model (Based on Vision Transformer) and ChromaDB"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93f1418a-4cdd-4866-964e-fd0b4d83d5f8",
   "metadata": {},
   "source": [
    "#### This Cookbook demonstrates A reverse image search or image similarity search, using an input image and some provided images which will be indexed or embedded in ChromaDB"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5939a54c-3198-4ba4-8346-1cc088c473c0",
   "metadata": {},
   "source": "##### You can embed text in the same VectorDB space as images, and retrieve text and images as well based on input text or image.\n##### Following link demonstrates that.\n<a> https://python.langchain.com/v0.2/docs/integrations/text_embedding/open_clip/ </a>"
  },
  {
   "cell_type": "markdown",
   "id": "32fbcbfe-92fa-4904-9a24-dd89d9e3865b",
   "metadata": {},
   "source": [
    "## Installs and imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b997cd5-7703-400d-a6a8-6ee09d37f7b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install langchain_experimental"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3a5078b2-b972-4866-b358-e5b33b129dc4",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install langchain_chroma"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "997c79e6-8a68-4aec-bf4d-1398e5e40389",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "from PIL import Image\n",
    "from tqdm import tqdm"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0118584-57d8-44e6-8129-89362c323141",
   "metadata": {},
   "source": [
    "### Langchain Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "957994c2-b4c0-4728-b6b6-dc580b9a8236",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import the Chroma class (any one of following works fine)\n",
    "from langchain_chroma import Chroma\n",
    "from langchain_experimental.open_clip import OpenCLIPEmbeddings\n",
    "# from langchain_community.vectorstores import Chroma"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2fbc2d8-4754-4155-b223-43528ed609be",
   "metadata": {},
   "source": [
    "## Provide your paths in a list"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c71b5712-e716-480b-8eef-65ef819ea17b",
   "metadata": {},
   "source": [
    "#### This Cookbook uses data from this Myntra Kaggle dataset :- <a> https://www.kaggle.com/datasets/hiteshsuthar101/myntra-fashion-product-dataset </a>\n",
    "#### You can directly download images or read the csv and links from it and then download"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "bd314d23-2994-475e-973b-3df7b315e23e",
   "metadata": {},
   "outputs": [],
   "source": [
    "all_image_uris = [\n",
    "    \"../../../py_ml_env/images_all/b0eb9426-adf2-4802-a6b3-5dbacbc5f2511643971561167KhushalKWomenBlackEthnicMotifsAngrakhaBeadsandStonesKurtawit7.jpg\",\n",
    "    \"../../../py_ml_env/images_all/17ab2ac8-2e60-422d-9d20-2527415932361640754214931-STRAPPY-SET-IN-ORANGE-WITH-ORGANZA-DUPATTA-5961640754214349-2.jpg\",\n",
    "    \"../../../py_ml_env/images_all/b8c4f90f-683c-48d2-b8ac-19891a87c0651638428628378KurtaSets1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/d2407657-1f04-4d13-9f52-9e134050489b1625905793495-Nayo-Women-Red-Ethnic-Motifs-Printed-Empire-Pure-Cotton-Kurt-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/30b0017d-7e72-4d40-9633-ef78d01719741575541717470-AHIKA-Women-Black--Green-Printed-Straight-Kurta-990157554171-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/507490f7-c8f9-492c-b3f8-c7e977d1af701654922515416SochWomenRedThreadWorkGeorgetteAnarkaliKurta1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/5fba9594-3301-4881-ba56-d56a44570e831654747998773LibasWomenNavyBluePureCottonFloralPrintKurtawithPalazzosDupa1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/e6b90907-a613-45e1-9b2e-988caaba36581645010770505-Ahalyaa-Women-Beige-Floral-Printed-Regular-Gotta-Patti-Kurta-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/5ea707f4-8491-4d1c-b520-86a1cff4c86e1644841891629-Anouk-Women-Yellow--White-Printed-Kurta-with-Palazzos-706164-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/11b842c5-d9d4-4fee-baa2-0972e3a673641643970773675KhushalKWomenGreenEthnicMotifsPrintedEmpireGottaPattiPureCot7.jpg\",\n",
    "    \"../../../py_ml_env/images_all/b783aef9-c902-462e-af73-de159bfd011c1565256752191-Libas-Women-Kurta-Sets-2081565256750830-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/bb925efb-80d9-4cb6-838c-df86f1ba3c3e1637570416652-Varanga-Women-Mustard-Yellow-Floral-Yoke-Embroidered-Straigh-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/7d7656e5-e37d-4f61-9407-98bd341ca8f91640261029836KurtaSets1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/43d65352-9853-498e-95a4-be514df0be901559294212152-Vishudh--Straight-Kurta-With-Crop-Palazzo-7041559294209627-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/4a37718e-8942-479c-a7ea-0b074d53ee4b1650456566424AnoukWomenPeach-ColouredYokeDesignMirror-WorkKurtawithTrouse1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/5910af54-3435-40d5-95d4-0ac2daf797f51658319613886-SheWill-Women-Maroon-Ethnic-Yoke-Design-Embroided-Kurta-with-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/d57adb8b-e792-477a-8801-6ea570cd88ef1629800170287VarangaWomenYellowFloralPrintedKeyholeNeckThreadWorkKurta1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/c35d059d-a357-4863-bcb1-eacd8c988fb01572422803188-AHIKA-Women-Kurtas-8841572422802083-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/3a61f2ab-7905-4efc-84e8-df1f74fa08201623409397327-Anouk-Women-Kurtas-1031623409396642-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/3e9c355b-20e6-42d0-8480-7046979f87711658733247220CharuWomenNavyBlueStripedThreadWorkKurta1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/0d391a8b-ea8c-4258-86d5-a99b9f3f34201630040200642-Libas-Women-Kurta-Sets-5941630040199555-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/d6b74d2b-825f-4b34-af01-9d6336045bdb1624612149604-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/07adcdf7-eee1-4077-b55c-f6608caaa6f01647663614971KALINIWomenSeaGreenFloralYokeDesignPleatedPureCottonTopwithS4.jpg\",\n",
    "    \"../../../py_ml_env/images_all/6bc412bb-3cc6-4def-8833-f5580b0cc06a1617706648250-Indo-Era-Green-Printed-Straight-Kurta-Palazzo-With-Dupatta-S-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/b1bd0687-7533-428d-8258-d29c793fc4541631092430795-Anouk-Women-Kurta-Sets-941631092429795-1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/64e975d5-dbda-4c09-87c0-c5152f9e82c71658736715566TOULINWomenTealFloralAngrakhaKurtiwithPalazzosWithDupatta1.jpg\",\n",
    "    \"../../../py_ml_env/images_all/d1a4cc48-ff90-47ab-ad36-800743e83d641605767381033-Ishin-Womens-Rayon-Red-Bandhani-Print-Embellished-Anarkali-K-1.jpg\",\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b2990fe-61e9-4c1d-9a53-cd6d9fcd82a3",
   "metadata": {},
   "source": [
    "## (Optional) Prepare Metadata to index alongside the image"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2b39f664-021d-4cea-aa65-bb575220fdbf",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[{'path': '../../../py_ml_env/images_all/b0eb9426-adf2-4802-a6b3-5dbacbc5f2511643971561167KhushalKWomenBlackEthnicMotifsAngrakhaBeadsandStonesKurtawit7.jpg', 'id': 0}, {'path': '../../../py_ml_env/images_all/17ab2ac8-2e60-422d-9d20-2527415932361640754214931-STRAPPY-SET-IN-ORANGE-WITH-ORGANZA-DUPATTA-5961640754214349-2.jpg', 'id': 1}, {'path': '../../../py_ml_env/images_all/b8c4f90f-683c-48d2-b8ac-19891a87c0651638428628378KurtaSets1.jpg', 'id': 2}, {'path': '../../../py_ml_env/images_all/d2407657-1f04-4d13-9f52-9e134050489b1625905793495-Nayo-Women-Red-Ethnic-Motifs-Printed-Empire-Pure-Cotton-Kurt-1.jpg', 'id': 3}, {'path': '../../../py_ml_env/images_all/30b0017d-7e72-4d40-9633-ef78d01719741575541717470-AHIKA-Women-Black--Green-Printed-Straight-Kurta-990157554171-1.jpg', 'id': 4}]\n"
     ]
    }
   ],
   "source": [
    "metadatas = []\n",
    "for idx, img in enumerate(all_image_uris):\n",
    "    meta_dict = {}\n",
    "    meta_dict[\"path\"] = img\n",
    "    meta_dict[\"id\"] = idx\n",
    "    metadatas.append(meta_dict)\n",
    "print(metadatas[:5])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb468009-38c8-45cf-8847-01dd6308cb62",
   "metadata": {},
   "source": [
    "## Initialize the OpenAI CLIP Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "7dab6d45-d3ba-4cf6-9738-737f8d5a8b5d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# You can use other models like Vit G 14, Vit H 14, Vit B32 etc.\n",
    "# Vit-L-14 - Larger , but more performant\n",
    "# ViT-B-32 - Smaller, less performant model\n",
    "\n",
    "# model_name = \"ViT-L-14\"\n",
    "# checkpoint = \"laion2b_s32b_b82k\"\n",
    "\n",
    "# Uncomment following to use that model\n",
    "model_name = \"ViT-B-32\"\n",
    "checkpoint = \"laion2b_s34b_b79k\"\n",
    "\n",
    "clip_embd = OpenCLIPEmbeddings(model_name=model_name, checkpoint=checkpoint)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9eb658f2-5d03-43f5-8bf0-8fc07feaca14",
   "metadata": {},
   "source": [
    "### Sample test of images"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8efb9fc3-639a-470e-8178-423b7b54bcac",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Embed images\n",
    "\n",
    "img_feat_1 = clip_embd.embed_image([all_image_uris[0]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8147e6c-f255-4db9-b63c-e178cc5a625c",
   "metadata": {},
   "source": [
    "### Dimentions of embeddings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "4c3a0d23-c8be-4e0a-8c31-e9f8a2ffd041",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "512"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(img_feat_1[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86bf2d60-57df-44d5-8cff-14e64304d411",
   "metadata": {},
   "source": [
    "### Initialize the Chroma Client, persist_directory is optinal if you want to save the VectorDB to disk and reload it using same code and path"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "35953afc-fb35-4dc9-842c-b756b80f4ec4",
   "metadata": {},
   "outputs": [],
   "source": [
    "collection_name = \"chroma_img_collection_1\"\n",
    "chroma_client = Chroma(\n",
    "    collection_name=collection_name,\n",
    "    embedding_function=clip_embd,\n",
    "    persist_directory=\"./indexed_db\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "710edbe8-a37c-4f52-a120-9e7d1f3dd351",
   "metadata": {},
   "outputs": [],
   "source": [
    "def embed_images(chroma_client, uris, metadatas=[]):\n",
    "    \"\"\"\n",
    "    Function to add images to Chroma client with progress bar.\n",
    "\n",
    "    Args:\n",
    "        chroma_client: The Chroma client object.\n",
    "        uris (List[str]): List of image file paths.\n",
    "        metadatas (List[dict]): List of metadata dictionaries.\n",
    "    \"\"\"\n",
    "    # Iterate through the uris with a progress bar\n",
    "    success_count = 0\n",
    "    for i in tqdm(range(len(uris)), desc=\"Adding images\"):\n",
    "        uri = uris[i]\n",
    "        metadata = metadatas[i]\n",
    "\n",
    "        try:\n",
    "            chroma_client.add_images(uris=[uri], metadatas=[metadata])\n",
    "        except Exception as e:\n",
    "            print(f\"Failed to add image {uri} with metadata {metadata}. Error: {e}\")\n",
    "        else:\n",
    "            success_count += 1\n",
    "            # print(f\"Successfully added image {uri} with metadata {metadata}\")\n",
    "\n",
    "    return success_count"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9fe96e05-ce8a-4272-a2e9-2ac39d9ae7dc",
   "metadata": {},
   "source": [
    "### Specify your image paths list in this embed_images function call"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "ed8e2663-6da1-454e-b552-18c762c0083d",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Adding images: 100%|████████████████████████████████████████████████████████████████████| 27/27 [00:03<00:00,  7.43it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "27 Images Embedded Successfully\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "success_count = embed_images(chroma_client, uris=all_image_uris, metadatas=metadatas)\n",
    "if success_count:\n",
    "    print(f\"{success_count} Images Embedded Successfully\")\n",
    "else:\n",
    "    print(\"No images Embedded\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e5cd014-db86-4d6b-8399-25cae3da5570",
   "metadata": {},
   "source": [
    "## Helper function to plot retrieved similar images"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "223ed942-5e68-4d62-908d-4cc7db1e7880",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "\n",
    "def plot_images_by_side(image_data):\n",
    "    num_images = len(image_data)\n",
    "    n_col = 2  # Fixed number of columns\n",
    "    n_row = math.ceil(num_images / n_col)  # Calculate the number of rows\n",
    "\n",
    "    # Reduce the size of each figure\n",
    "    fig, axs = plt.subplots(n_row, n_col, figsize=(10, 5 * n_row))\n",
    "    axs = axs.flatten()\n",
    "\n",
    "    for idx, data in enumerate(image_data):\n",
    "        img_path = data[\"path\"]\n",
    "        score = round(data.get(\"score\", 0), 2)\n",
    "        img = Image.open(img_path)\n",
    "        ax = axs[idx]\n",
    "        ax.imshow(img)\n",
    "        # Assuming similarity is not available in the new data, removed sim_score\n",
    "        ax.title.set_text(f\"\\nProduct ID: {data['id']}\\n Score: {score}\")\n",
    "        ax.axis(\"off\")  # Turn off axis\n",
    "\n",
    "    # Hide any remaining empty subplots\n",
    "    for i in range(num_images, n_row * n_col):\n",
    "        axs[i].axis(\"off\")\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca14bbde-cb91-4bb9-a766-7eecd1f903a6",
   "metadata": {},
   "source": [
    "## Take in input image path, resize that image and display it"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "1d402b25-ba85-4ef1-80bf-628c90c8e4f8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "[encoded data, 22504 characters]\n",
      "image/png": "[encoded data, 204260 characters]\n",
      "text/plain": [
       "<PIL.Image.Image image mode=RGB size=300x400>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "search_img_path = \"../../../py_ml_env/images_all/0d391a8b-ea8c-4258-86d5-a99b9f3f34201630040200642-Libas-Women-Kurta-Sets-5941630040199555-1.jpg\"\n",
    "\n",
    "my_image = Image.open(search_img_path).convert(\"RGB\")\n",
    "# Resize the image while maintaining the aspect ratio\n",
    "max_width = 400\n",
    "max_height = 400\n",
    "\n",
    "width, height = my_image.size\n",
    "aspect_ratio = width / height\n",
    "\n",
    "if width > height:\n",
    "    new_width = min(width, max_width)\n",
    "    new_height = int(new_width / aspect_ratio)\n",
    "else:\n",
    "    new_height = min(height, max_height)\n",
    "    new_width = int(new_height * aspect_ratio)\n",
    "\n",
    "my_image_resized = my_image.resize((new_width, new_height), Image.LANCZOS)\n",
    "\n",
    "# Display the resized image\n",
    "my_image_resized"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f66ee680-27d2-4f53-b0c8-792cb97c98a2",
   "metadata": {},
   "source": [
    "## Perform Image similarity search, get the metadata of K retrieved images and then display similar images"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e4261cae-30e0-435f-a497-0b7f3f11f353",
   "metadata": {},
   "source": [
    "### We have embeded limited data, we can embed a large number which will have similar images, to get better results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "ac0c5574-9dc5-4bd9-a5dd-2d34a274684d",
   "metadata": {},
   "outputs": [],
   "source": [
    "k = 10\n",
    "\n",
    "## This returns a list of Langchain document object, with page_content as the base64 encoded image, this approach uses path from metadata to display images\n",
    "## We can use that b64 encoded images as well after decoding it\n",
    "\n",
    "similar_images = chroma_client.similarity_search_by_image(uri=search_img_path, k=k)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "6c812ea0-e9d3-4539-ae08-30ac4f7cd9da",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 2160180 characters]",
      "text/plain": [
       "<Figure size 1000x2500 with 10 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "similar_image_data_1 = []\n",
    "for img in similar_images:\n",
    "    # Get metadata from Doc object\n",
    "    similar_image_data_1.append(img.metadata)\n",
    "plot_images_by_side(similar_image_data_1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de02016c-3961-457c-8198-160a1ec99af0",
   "metadata": {},
   "source": [
    "## Perform similarity search with image with relevance scores:\n",
    " We get a list of K tuples like following:\n",
    " [\n",
    "    (Langchain_Document,score),\n",
    "   (Langchain_Document,score),\n",
    "   Langchain_Document,score)\n",
    "   ]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "ea0b5f77-e6b7-4721-aee6-6aa1ff0d8e29",
   "metadata": {},
   "outputs": [],
   "source": [
    "similar_images = chroma_client.similarity_search_by_image_with_relevance_score(\n",
    "    uri=search_img_path, k=k\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "d6400bb7-9a3f-4472-b01a-1cb9246e0a51",
   "metadata": {},
   "outputs": [],
   "source": [
    "similar_image_data_2 = []\n",
    "for img in similar_images:\n",
    "    # Get metadata from Doc object\n",
    "    meta_dict = img[0].metadata\n",
    "    # Add score to it\n",
    "    meta_dict[\"score\"] = img[1]\n",
    "    similar_image_data_2.append(meta_dict)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "294fe7ff-ffba-4386-9c27-d40581fa4c6b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 2164924 characters]",
      "text/plain": [
       "<Figure size 1000x2500 with 10 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_images_by_side(similar_image_data_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c8917bf-f84b-49f3-ae66-da0e78644139",
   "metadata": {},
   "source": [
    "## We have successfully implemented an image-to-image search using CLIP and ChromaDB !"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}