diff --git a/notebooks/Convert_Documents_to_Stylized_HTML_using_the_Unstructured_API.ipynb b/notebooks/Convert_Documents_to_Stylized_HTML_using_the_Unstructured_API.ipynb new file mode 100644 index 0000000..4837ce0 --- /dev/null +++ b/notebooks/Convert_Documents_to_Stylized_HTML_using_the_Unstructured_API.ipynb @@ -0,0 +1,1429 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "e8A6SSXO3433" + }, + "source": [ + "# Convert Your Documents to Stylized HTML using the Unstructured API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PGVBj7Po38lx" + }, + "source": [ + "### Why convert to HTML?\n", + "Do you have lots of unstructured data sources (PDFs, PPTs, scanned docs, etc.) that you want to source from and display within a unified visual interface? \n", + "\n", + "Perhaps you are building a chatbot and want to be able to visually render snippets of the sources within the web app? \n", + "\n", + "Or perhaps you want to render an entire HTML representation of the source document? \n", + "\n", + "> Ultimately, this functionality is perfect for web apps, knowledge bases, or downstream AI workflows.\n", + "\n", + "By converting all those diverse documents to HTML first, your application code can remain clean, minimal, and the UX experience much more consistent!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### How do I actually convert my docs to HTML?\n", + "If you just use a vanilla VLM to convert your docs, you're going to have a difficult time consistently rendering their outputs, and if you want to set it up on a production pipeline, there's a lot of orchestration involved. With the Unstructured ETL+ platform, you can easily create data pipelines for your business with an intuitive visual user interface. You can sign up for our [free 1-week trial here](https://unstructured.io/pricing?modal=try-for-free).\n", + "\n", + "\n", + "This notebook walks you through using the [Unstructured ETL+ platform](https://platform.unstructured.io) via API to instantly convert PDFs, Powerpoints, and scanned documents into a serialized JSON representation of the document (perfect for RAG or Retrieval Augmented Generation) using its VLM strategy. However, this JSON also contains within its metadata a high-fidelity HTML representation of your document.\n", + "\n", + "We will then extract and reconstitute that HTML into a single file and then finally apply CSS styling to the file. For the CSS styling, we offer two themes to choose from: dark and light, but you will likely want to modify these to coincide with your company's branding colors! " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### NOTE: Only the `VLM` Strategy of the Unstructured's (4) Parsing Strategies Generates HTML\n", + "\n", + "The Unstructured ETL+ platform features 4 partitioning (aka parsing) strategies: `auto`, `vlm`, `hi_res`, & `fast`. \n", + "\n", + "`auto`: Default; routes files/pages dynamically for best quality at lowest cost.\n", + "\n", + "`vlm`: Highest-quality transformation for images, complex PDFs, and powerpoint presentations.\n", + "\n", + "`high_res`: For other file types or when bounding box coordinates are needed.\n", + "\n", + "`fast`: For text-only documents; rule-based, very quick and cheap.\n", + "\n", + "Of all of these, only the `vlm` strategy contains a full `html` representation of the documents it parses, so the other strategies cannot be used with this notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "### 1. Unstructured API Key\n", + "\n", + "Create an API key from the [Unstructured Platform](https://platform.unstructured.io)\n", + "\n", + "1. Sign up for our [free trial](https://unstructured.io/pricing?modal=try-for-free) to get access or log in if you're already a user.\n", + "2. Once logged in, go to the **API Keys** section in the sidebar or [click here](https://platform.unstructured.io/app/account/api-keys).\n", + "3. Click **New Key**, name it something fitting to your use case (or just e.g. `extract-html-example-key` if you don't know your use case yet), and copy it securely.\n", + "4. Once you have your API key created and copied, add it as either a secret or environment variable under the name `UNSTRUCTURED_API_KEY`:\n", + " * If running this notebook within Google Colab: Within the UI (left sidebar), click `Secrets` > `Add New Secret` > `UNSTRUCTURED_API_KEY = 'paste-your-key-here'`; \n", + " * If running this notebook locally, save the key as an environment variable, e.g. `export UNSTRUCTURED_API_KEY = 'paste-your-key-here'` in the kernel context you will be running your jupyter notebook\n", + "\n", + "### 2. A Sample Document\n", + "\n", + "If using this notebook directly from Google Colab, then upload your document directly to Colab. Otherwise, if running this notebook locally, you can just have your document locally available on your computer. The document must be a document type supported by our VLM strategy: `pdf`, `ppt`, `pptx`, `jpg`, `jpeg`, `png`, `tiff`, `heic`, `webp`.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "InQdUxEo57_i" + }, + "source": [ + "## Step 1: Parse Your Documents using the VLM strategy of the Unstructured API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z6AanqO87FCA" + }, + "outputs": [], + "source": [ + "# Some pip installs to visualize the pdf pages directly in the notebook:\n", + "!pip install --upgrade pip\n", + "!pip install pdf2image\n", + "!apt-get install poppler-utils # if you're on a linux machine and/or for Google Colab\n", + "#!brew install poppler # if you're on a mac, running the notebook locally\n", + "#!choco install poppler # if you're on windows, running the notebook locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "64R5LsOe7I2T" + }, + "outputs": [], + "source": [ + "import requests\n", + "import os\n", + "from pathlib import Path\n", + "# Check if the notebook is running in Google Colab vs locally\n", + "try:\n", + " from google.colab import userdata\n", + " GOOGLE_COLAB = True\n", + " from google.colab import files as google_colab_files\n", + "except ImportError:\n", + " GOOGLE_COLAB = False\n", + "\n", + "\n", + "# Platform Partition URL\n", + "PARTITION_API_URL = \"https://api.unstructuredapp.io/general/v0/general\"\n", + "\n", + "# Your API key loaded from Google Colab Secrets or your environment variable\n", + "UNSTRUCTURED_API_KEY = userdata.get(\"UNSTRUCTURED_API_KEY\") if GOOGLE_COLAB else os.getenv(\"UNSTRUCTURED_API_KEY\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MrcJwDay6ZU7" + }, + "outputs": [], + "source": [ + "# List of file types that are eligible for VLM conversion\n", + "# NOTE: Over time, we will expand this list to include more file types, but for now, this is the list\n", + "# of file types that are currently supported by the VLM strategy.\n", + "VLM_ELIGIBLE_FILE_TYPES = [\"pdf\", \"ppt\", \"pptx\", \"jpg\", \"jpeg\", \"png\", \"tiff\", \"heic\", \"webp\"]\n", + "\n", + "# List of supported models for VLM conversion\n", + "SUPPORTED_MODELS = [\n", + " (\"anthropic\", \"claude-3-5-sonnet-20241022\"),\n", + " (\"openai\", \"gpt-4o\"),\n", + " (\"google\", \"gemini-1.5-pro\"),\n", + " (\"vertexai\", \"gemini-2.0-flash-001\"),\n", + " (\"azure_openai\", \"gpt-4o\"),\n", + " (\"anthropic_bedrock\", \"claude-3-5-sonnet-20241022\"),\n", + " (\"bedrock\", \"us.amazon.nova-pro-v1:0\"),\n", + " (\"bedrock\", \"us.amazon.nova-lite-v1:0\"),\n", + " (\"bedrock\", \"us.anthropic.claude-3-5-sonnet-20241022-v2:0\"),\n", + " (\"bedrock\", \"us.anthropic.claude-3-opus-20240229-v1:0\"),\n", + " (\"bedrock\", \"us.anthropic.claude-3-haiku-20240307-v1:0\"),\n", + " (\"bedrock\", \"us.anthropic.claude-3-sonnet-20240229-v1:0\"),\n", + " (\"bedrock\", \"us.meta.llama3-2-90b-instruct-v1:0\"),\n", + " (\"bedrock\", \"us.meta.llama3-2-11b-instruct-v1:0\"),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mJbWm-Ek3DJU" + }, + "outputs": [], + "source": [ + "# Set the headers for the API request, including the API key\n", + "api_headers = {\"accept\": \"application/json\", \"unstructured-api-key\": UNSTRUCTURED_API_KEY}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "reFrTEbl3zJo" + }, + "source": [ + "### Partition A Local Document via the Unstructured API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tggNSeGH3z43" + }, + "outputs": [], + "source": [ + "# Set the filename to the path of the file you want to convert\n", + "filename = \"content/Your-Example-Local-PDF-Here.pdf\"\n", + "\n", + "# Select the model you want to use for the VLM conversion\n", + "provider, model = SUPPORTED_MODELS[0]\n", + "partition_parameters = {\"strategy\": \"vlm\", \"vlm_model_provider\": provider, \"vlm_model\": model}\n", + "\n", + "# Rememember to make sure the file type is supported by the VLM conversion strategy\n", + "extension = Path(filename).suffix.lower().split(\".\")[-1]\n", + "if extension not in VLM_ELIGIBLE_FILE_TYPES:\n", + " raise ValueError(f\"File type {extension} is not supported for VLM conversion. Please use a supported file type.\")\n", + "\n", + "# Open the file and send it to the Unstructured API\n", + "with open(filename,'rb') as file:\n", + " file_payload = {\"files\": (filename, file)}\n", + " response = requests.post(\n", + " PARTITION_API_URL, \n", + " headers=api_headers, \n", + " files=file_payload, \n", + " data=partition_parameters\n", + " )\n", + "print(f\"Response Code for {provider} - {model}: {response.status_code}\")\n", + "file_as_json = response.json()\n", + "\n", + "# The `file_as_json` is a JSON representation of the document's contents, and each element \n", + "# has a `metadata` field that contains the HTML representation of the element: `metadata.text_as_html`\n", + "# Print the first element of the JSON array to verify that the HTML field is present.\n", + "print(file_as_json[:1])\n", + "print(file_as_json[0]['metadata']['text_as_html'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To learn more about our standard JSON output, you can read more about it in our documentation here: [Unstructured Document Elements](https://docs.unstructured.io/api-reference/partition/document-elements)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0f0O_YsuFGY2" + }, + "source": [ + "## Step 2: Convert the JSON representation of the file into an HTML representation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bSGvMamLFSy3" + }, + "outputs": [], + "source": [ + "from typing import Dict, List, Optional\n", + "\n", + "# Object-oriented class to serve as a converter for the VLM JSON data to structured HTML\n", + "class VlmJsonToHtmlConverter:\n", + " \"\"\"Converts VLM JSON data to structured HTML.\"\"\"\n", + "\n", + " def __init__(self, title: Optional[str] = None):\n", + " self.title = title\n", + " self.pages: Dict[int, List[str]] = {}\n", + " self.document_title = \"Converted Document\"\n", + " self._theme_css = \"\"\n", + "\n", + " def convert(self, json_data: List[Dict]) -> str:\n", + " \"\"\"Convert JSON data to HTML document.\"\"\"\n", + " if not json_data:\n", + " raise ValueError(\"JSON data cannot be empty\")\n", + "\n", + " self._process_elements(json_data)\n", + " return self._build_document()\n", + "\n", + " def _process_elements(self, data: List[Dict]) -> None:\n", + " \"\"\"Process JSON elements and organize by pages.\"\"\"\n", + " for element in data:\n", + " if 'metadata' not in element:\n", + " continue\n", + "\n", + " metadata = element['metadata']\n", + " self._extract_title(metadata)\n", + "\n", + " page_num = metadata.get('page_number', 1)\n", + " html_content = self._extract_content(element, metadata)\n", + "\n", + " if html_content:\n", + " self._add_to_page(page_num, html_content)\n", + "\n", + " def _extract_title(self, metadata: Dict) -> None:\n", + " \"\"\"Extract document title from metadata if not set.\"\"\"\n", + " if self.title is None and 'filename' in metadata:\n", + " self.document_title = metadata['filename']\n", + " self.title = self.document_title\n", + "\n", + " def _extract_content(self, element: Dict, metadata: Dict) -> Optional[str]:\n", + " \"\"\"Extract HTML content from element.\"\"\"\n", + " # Skip self-closing Page divs\n", + " if self._is_page_div_artifact(metadata):\n", + " return None\n", + "\n", + " # Prioritize structured HTML\n", + " if 'text_as_html' in metadata:\n", + " return metadata['text_as_html']\n", + "\n", + " # Convert plain text to HTML\n", + " if 'text' in element and element['text'] is not None:\n", + " element_type = element.get('type', 'div')\n", + " return f'<{element_type} class=\"{element_type}\">{element[\"text\"]}'\n", + "\n", + " return None\n", + "\n", + " def _is_page_div_artifact(self, metadata: Dict) -> bool:\n", + " \"\"\"Check if content is a self-closing Page div artifact.\"\"\"\n", + " return ('text_as_html' in metadata and\n", + " '
' in metadata['text_as_html'])\n", + "\n", + " def _add_to_page(self, page_num: int, content: str) -> None:\n", + " \"\"\"Add content to specified page.\"\"\"\n", + " if page_num not in self.pages:\n", + " self.pages[page_num] = []\n", + " self.pages[page_num].append(content)\n", + "\n", + " def _build_document(self) -> str:\n", + " \"\"\"Build complete HTML document from pages.\"\"\"\n", + " page_blocks = [\n", + " f'
\\n{\"\".join(content)}\\n
'\n", + " for num in sorted(self.pages.keys())\n", + " for content in [self.pages[num]]\n", + " ]\n", + "\n", + " body_content = '\\n'.join(page_blocks)\n", + " css_section = f\"\\n \" if self._theme_css else \"\"\n", + "\n", + " return f\"\"\"\n", + "\n", + "\n", + " \n", + " {self.document_title}\n", + " {css_section}\n", + "\n", + "\n", + "{body_content}\n", + "\n", + "\"\"\"\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mQIPdtuAKqIj" + }, + "outputs": [], + "source": [ + "file_as_html = VlmJsonToHtmlConverter(title).convert(json_data)\n", + "print(file_as_html)\n", + "\n", + "# You can also save the file and open it in a browser to see!\n", + "html_file_name = f\"{filename}.html\"\n", + "with open(html_file_name, \"w\") as file:\n", + " file.write(file_as_html)\n", + "\n", + "if GOOGLE_COLAB:\n", + " google_colab_files.download(html_file_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gy5_xn15IgN1" + }, + "source": [ + "## Step 3: Stylize the outputs\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nVTBIOtEIkKf" + }, + "source": [ + "Now that we have our HTML, the document can be visualized. However, if you did save the html to a file and open it in a browser, you will notice it is very basic.\n", + "\n", + "If you want to actually display the HTML contents in a production application, you may wish to present it with some basic styling. This is done by adding CSS formatting to it. Because Unstructured's HTML leverages a standardized ontology, you can use that to create visually appealing CSS styling!\n", + "\n", + "\n", + "We've provided a dark theme and light theme as a starting point, but feel free to modify or customize however you see fit for your company's brand or use case.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u3Ea0886IjW4" + }, + "outputs": [], + "source": [ + "LIGHT_MODE_CSS_STYLING = \"\"\"\n", + " :root {\n", + " --bg-primary: #f5f5f5;\n", + " --bg-secondary: rgba(235, 254, 225, 0.078);\n", + " --bg-tertiary: rgba(240, 234, 230, 0.65);\n", + " --bg-teal: #2b8b9c;\n", + " --bg-darker-teal: #04cbef;\n", + " --bg-dark-sky-blue: #74b0ec;\n", + " --bg-light-sky-blue: #bbe5fa;\n", + " --bg-cloud-light-white: #f8f5f7;\n", + " --bg-cloud-light-pink: #f0e8e8e6;\n", + " --bg-cloud-light-yellow: #fcf8ee;\n", + " --bg-dark: rgba(9, 26, 29, 0.788);\n", + " --bg-darker: rgba(11, 34, 39, 0.862);\n", + "\n", + "\n", + " /* Gradient overlays */\n", + " --overlay-blue: rgba(104, 187, 243, 0.03);\n", + " --overlay-teal: rgba(101, 218, 244, 0.04);\n", + "\n", + " /* Text colors */\n", + " --text-primary: #030e10;\n", + " --text-secondary: #162232;\n", + " --text-muted: rgba(225, 243, 254, 0.6);\n", + " --text-light: #f5f5f5;\n", + " --text-blue: #148edf;\n", + " --text-dark-blue: #056baf;\n", + " --text-teal: #04caf6;\n", + " --text-dark-teal: #0289a7;\n", + " /* Accent colors */\n", + " --accent-blue: #68bbf3;\n", + " --accent-teal: #65daf4;\n", + " --accent-lime: #8bfaad;\n", + " --accent-yellow: rgba(253, 246, 128, 0.85);\n", + " --accent-pink: rgba(232, 167, 225, 0.75);\n", + " --accent-red: rgba(236, 98, 92, 0.9);\n", + "\n", + " /* Warm accent palette */\n", + " --accent-peach: rgba(232, 192, 170, 0.85);\n", + " --accent-peach-dark: rgba(146, 96, 69, 0.85);\n", + " --accent-pink-red: rgba(192, 160, 158, 0.8);\n", + " --accent-soft-peach: rgba(236, 225, 218, 0.282);\n", + "\n", + " /* Subtle colors */\n", + " --border-light: rgba(225, 243, 254, 0.08);\n", + " --border-medium: rgba(225, 243, 254, 0.12);\n", + "\n", + " /* Warm gradients */\n", + " --gradient-warm: linear-gradient(105deg,\n", + " rgba(232, 192, 170, 0.05),\n", + " rgba(236, 98, 92, 0.05));\n", + " }\n", + "\n", + " body.Document {\n", + " background-color: var(--bg-primary);\n", + " color: var(--text-primary);\n", + " font-family: Arial, sans-serif;\n", + " line-height: 1.6;\n", + " min-height: 100vh;\n", + " }\n", + "\n", + " .Section,\n", + " .Page,\n", + " .Column {\n", + " margin-bottom: 20px;\n", + " }\n", + "\n", + " .Page {\n", + " margin-bottom: 40px;\n", + " padding: 30px;\n", + " background-color: radial-gradient(var(--accent-yellow),\n", + " var(--accent-pink));\n", + " border-radius: 12px;\n", + " box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\n", + " position: relative;\n", + " border: 2px solid var(--bg-darker-teal);\n", + " backdrop-filter: blur(20px);\n", + " }\n", + "\n", + " .Page::after {\n", + " content: attr(data-page-number);\n", + " position: absolute;\n", + " bottom: 15px;\n", + " right: 15px;\n", + " font-size: 0.8em;\n", + " color: var(--accent-teal);\n", + " background: linear-gradient(115deg, var(--bg-secondary), var(--bg-tertiary));\n", + " padding: 4px 12px;\n", + " border-radius: 6px;\n", + " border: 1px solid var(--border-light);\n", + " }\n", + "\n", + " .Page:not(:last-child)::before {\n", + " content: '';\n", + " display: block;\n", + " height: 1px;\n", + " background-color: var(--border-medium);\n", + " margin: 15px 0;\n", + " }\n", + "\n", + " .Section {\n", + " background: linear-gradient(100deg,\n", + " var(--bg-cloud-light-white),\n", + " var(--bg-cloud-light-yellow),\n", + " var(--bg-cloud-light-pink));\n", + " border-radius: 12px;\n", + " padding: 10px;\n", + " margin-bottom: 30px;\n", + " border: 2px solid var(--text-dark-teal);\n", + " display: flex;\n", + " flex-wrap: wrap;\n", + " flex-direction: column;\n", + " justify-content: space-between;\n", + " filter: drop-shadow(0 0 0.75rem rgba(243, 212, 218, 0.189));\n", + " }\n", + "\n", + " .Column {\n", + " flex: 1;\n", + " min-width: 0;\n", + " padding: 0 10px;\n", + " }\n", + "\n", + " .Section.two-column .Column {\n", + " flex-basis: calc(50% - 20px);\n", + " }\n", + "\n", + " .Section.three-column .Column {\n", + " flex-basis: calc(33.333% - 20px);\n", + " }\n", + "\n", + " .Section.four-column .Column {\n", + " flex-basis: calc(25% - 20px);\n", + " }\n", + "\n", + " .Section.five-column .Column {\n", + " flex-basis: calc(20% - 20px);\n", + " }\n", + " .Section .Title {\n", + " width: 100%;\n", + " }\n", + "\n", + " .Header,\n", + " .Footer,\n", + " .Sidebar {\n", + " padding: 10px;\n", + " border-radius: 5px;\n", + " background-color: linear-gradient(100deg,\n", + " var(--accent-pink),\n", + " var(--accent-yellow));\n", + " }\n", + "\n", + " .PageBreak {\n", + " border: none;\n", + " border-top: 1px dashed var(--border-medium);\n", + " margin: 20px 0;\n", + " }\n", + "\n", + " .Title {\n", + " color: var(--text-blue);\n", + " margin-bottom: 20px;\n", + " font-size: 2em;\n", + " letter-spacing: -0.02em;\n", + " border-radius: 10px;\n", + " /* width: 100%; */\n", + " }\n", + "\n", + " .Subtitle {\n", + " color: var(--accent-red);\n", + " margin-bottom: 15px;\n", + " font-size: 1.2em;\n", + " letter-spacing: 0.01em;\n", + " }\n", + "\n", + " .Heading {\n", + " color: var(--accent-blue);\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .NarrativeText,\n", + " .UncategorizedText {\n", + " margin-bottom: 10px;\n", + " }\n", + "\n", + " .Quote {\n", + " border-left: 3px solid var(--accent-lime);\n", + " padding: 20px;\n", + " margin: 20px 0;\n", + " background: linear-gradient(90deg,\n", + " rgba(139, 250, 173, 0.08),\n", + " rgba(232, 192, 170, 0.03));\n", + " border-radius: 0 8px 8px 0;\n", + " }\n", + "\n", + " .Footnote,\n", + " .Caption,\n", + " .PageNumber {\n", + " font-size: 0.9em;\n", + " color: var(--text-muted);\n", + " }\n", + "\n", + " .OrderedList,\n", + " .UnorderedList,\n", + " .DefinitionList {\n", + " margin-left: 20px;\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .ListItem {\n", + " margin-bottom: 5px;\n", + " list-style-type: circle;\n", + " }\n", + "\n", + " .Table {\n", + " border-collapse: separate;\n", + " border-spacing: 0;\n", + " width: 100%;\n", + " border-radius: 8px;\n", + " overflow: hidden;\n", + " border: 1px solid var(--accent-blue);\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .TableRow,\n", + " .TableCell {\n", + " border: 1px solid var(--accent-blue);\n", + " padding: 12px 16px;\n", + " }\n", + "\n", + " .TableHeader {\n", + " background: linear-gradient(90deg, var(--bg-dark-sky-blue), var(--bg-light-sky-blue));\n", + " color: var(--text-dark);\n", + " font-weight: 500;\n", + " padding: 12px 16px;\n", + " }\n", + "\n", + " .Image,\n", + " .Figure,\n", + " .Video,\n", + " .Audio,\n", + " .Barcode,\n", + " .QRCode,\n", + " .Logo {\n", + " height: auto;\n", + " margin-bottom: 5px;\n", + " font-style: italic;\n", + " }\n", + "\n", + " .CodeBlock,\n", + " .InlineCode {\n", + " font-family: 'Fira Code', monospace;\n", + " background: var(--bg-tertiary);\n", + " padding: 20px;\n", + " border-radius: 8px;\n", + " border: 1px solid var(--border-medium);\n", + " color: var(--accent-lime);\n", + " margin: 20px 0;\n", + " }\n", + "\n", + " .Formula,\n", + " .Equation {\n", + " font-style: italic;\n", + " margin: 10px 0;\n", + " }\n", + "\n", + " .FootnoteReference,\n", + " .Citation,\n", + " .Bibliography,\n", + " .Glossary {\n", + " color: var(--accent-teal);\n", + " }\n", + "\n", + " .Author,\n", + " .Date,\n", + " .Keywords {\n", + " color: var(--text-secondary);\n", + " font-size: 0.9em;\n", + " }\n", + "\n", + " .TableOfContents,\n", + " .Index {\n", + " background: var(--bg-tertiary);\n", + " padding: 20px;\n", + " border-radius: 8px;\n", + " margin-bottom: 15px;\n", + " border: 1px solid var(--border-medium);\n", + " }\n", + "\n", + " .Form {\n", + " margin-bottom: 15px;\n", + " padding: 10px;\n", + " border-radius: 20px;\n", + " border: 1px solid var(--bg-dark-sky-blue);\n", + " background: linear-gradient(180deg,\n", + " var(--bg-cloud-light-white),\n", + " var(--text-muted))\n", + " }\n", + "\n", + " .FormField {\n", + " border: 1px solid var(--accent-red);\n", + " padding: 12px;\n", + " border-radius: 6px;\n", + " font-weight: 800;\n", + " color: var(--text-primary);\n", + " transition: all 0.2s ease;\n", + " display: block;\n", + " margin-bottom: 10px;\n", + " background: linear-gradient(165deg,\n", + " var(--bg-cloud-light-white),\n", + " var(--bg-cloud-light-pink));\n", + " }\n", + "\n", + " .FormFieldValue {\n", + " padding: 0px 8px 0px 8px;\n", + " border-radius: 9px;\n", + " background-color: var(--accent-soft-peach);\n", + " color: var(--text-dark);\n", + " border: 1px solid var(--accent-peach-dark);\n", + " }\n", + "\n", + " .Button {\n", + " background: linear-gradient(135deg, var(--accent-blue), var(--accent-teal));\n", + " color: var(--bg-primary);\n", + " border: none;\n", + " padding: 10px 20px;\n", + " border-radius: 6px;\n", + " cursor: pointer;\n", + " font-weight: 500;\n", + " transition: all 0.3s ease;\n", + " box-shadow: 0 2px 8px rgba(104, 187, 243, 0.2);\n", + " }\n", + "\n", + " .Button:hover {\n", + " transform: translateY(-1px);\n", + " box-shadow: 0 4px 12px rgba(104, 187, 243, 0.3);\n", + " }\n", + "\n", + " .Button.critical {\n", + " background: linear-gradient(135deg,\n", + " var(--accent-red),\n", + " var(--accent-pink-red));\n", + " }\n", + "\n", + " .Comment {\n", + " color: var(--accent-lime);\n", + " font-style: italic;\n", + " }\n", + "\n", + " .Highlight {\n", + " background: linear-gradient(120deg,\n", + " rgba(253, 246, 128, 0.1),\n", + " rgba(232, 167, 225, 0.1));\n", + " padding: 2px 6px;\n", + " border-radius: 4px;\n", + " }\n", + "\n", + " .RevisionInsertion {\n", + " color: var(--accent-lime);\n", + " text-decoration: none;\n", + " border-bottom: 1px dashed rgba(139, 250, 173, 0.4);\n", + " }\n", + "\n", + " .RevisionDeletion {\n", + " color: var(--accent-red);\n", + " text-decoration: line-through;\n", + " border-bottom: 1px dashed rgba(236, 98, 92, 0.4);\n", + " }\n", + "\n", + " .Address,\n", + " .EmailAddress,\n", + " .PhoneNumber,\n", + " .Date,\n", + " .Time,\n", + " .Currency,\n", + " .Measurement {\n", + " color: var(--accent-peach-dark);\n", + " }\n", + "\n", + " .Date.important {\n", + " color: var(--accent-pink-red);\n", + " font-weight: 500;\n", + " }\n", + "\n", + " .Letterhead,\n", + " .Signature,\n", + " .Watermark,\n", + " .Stamp {\n", + " opacity: 0.7;\n", + " }\n", + "\n", + " .Hyperlink {\n", + " color: var(--accent-teal);\n", + " text-decoration: none;\n", + " transition: all 0.2s ease;\n", + " border-bottom: 1px solid rgba(101, 218, 244, 0.2);\n", + " }\n", + "\n", + " .Hyperlink:hover {\n", + " border-bottom-color: var(--accent-teal);\n", + " }\n", + "\n", + " .Hyperlink.featured {\n", + " background: linear-gradient(90deg,\n", + " var(--accent-peach),\n", + " var(--accent-pink));\n", + " -webkit-text-fill-color: transparent;\n", + " border-bottom: none;\n", + " }\n", + "\n", + " .Alert {\n", + " background: linear-gradient(135deg,\n", + " rgba(236, 98, 92, 0.08),\n", + " rgba(232, 167, 225, 0.05));\n", + " border-left: 3px solid var(--accent-red);\n", + " padding: 16px 20px;\n", + " border-radius: 0 8px 8px 0;\n", + " margin: 20px 0;\n", + " }\n", + "\n", + " .Priority {\n", + " display: inline-block;\n", + " padding: 2px 8px;\n", + " border-radius: 4px;\n", + " font-size: 0.85em;\n", + " font-weight: 500;\n", + " }\n", + "\n", + " .Priority.high {\n", + " background: rgba(236, 98, 92, 0.1);\n", + " color: var(--accent-red);\n", + " border: 1px solid rgba(236, 98, 92, 0.2);\n", + " }\n", + "\n", + " .Priority.medium {\n", + " background: rgba(232, 167, 225, 0.1);\n", + " color: var(--accent-pink);\n", + " border: 1px solid rgba(232, 167, 225, 0.2);\n", + " }\n", + "\n", + " .Tag {\n", + " display: inline-block;\n", + " padding: 3px 10px;\n", + " border-radius: 12px;\n", + " font-size: 0.85em;\n", + " margin: 0 4px;\n", + " background: var(--bg-tertiary);\n", + " }\n", + "\n", + " .Tag.featured {\n", + " background: rgba(232, 192, 170, 0.1);\n", + " color: var(--accent-peach);\n", + " border: 1px solid rgba(232, 192, 170, 0.15);\n", + " }\n", + "\n", + " .Progress {\n", + " height: 4px;\n", + " background: var(--bg-tertiary);\n", + " border-radius: 2px;\n", + " overflow: hidden;\n", + " }\n", + "\n", + " .Progress .bar {\n", + " height: 100%;\n", + " background: linear-gradient(90deg,\n", + " var(--accent-blue),\n", + " var(--accent-pink));\n", + " transition: width 0.3s ease;\n", + " }\n", + "\n", + " .Decorator {\n", + " position: absolute;\n", + " width: 100px;\n", + " height: 100px;\n", + " opacity: 0.03;\n", + " filter: blur(40px);\n", + " border-radius: 50%;\n", + " pointer-events: none;\n", + " }\n", + "\n", + " ::selection {\n", + " background: rgba(232, 167, 225, 0.2);\n", + " color: var(--text-primary);\n", + " }\n", + "\"\"\"\n", + "DARK_MODE_CSS_STYLING = \"\"\"\n", + " :root {\n", + " --bg-primary: #030e10;\n", + " --bg-secondary: #0a1f24;\n", + " --bg-tertiary: #162232;\n", + "\n", + " /* Gradient overlays */\n", + " --overlay-blue: rgba(104, 187, 243, 0.03);\n", + " --overlay-teal: rgba(101, 218, 244, 0.04);\n", + "\n", + " /* Text colors */\n", + " --text-primary: #f5f5f5;\n", + " --text-secondary: rgba(225, 243, 254, 0.85);\n", + " --text-muted: rgba(225, 243, 254, 0.6);\n", + " --text-dark: #030e10;\n", + "\n", + " /* Accent colors */\n", + " --accent-blue: #68bbf3;\n", + " --accent-teal: #65daf4;\n", + " --accent-lime: #8bfaad;\n", + " --accent-yellow: rgba(253, 246, 128, 0.85);\n", + " --accent-pink: rgba(232, 167, 225, 0.75);\n", + " --accent-red: rgba(236, 98, 92, 0.9);\n", + "\n", + " /* Warm accent palette */\n", + " --accent-peach: rgba(232, 192, 170, 0.85);\n", + " --accent-pink-red: rgba(192, 160, 158, 0.8);\n", + "\n", + " /* Subtle colors */\n", + " --border-light: rgba(225, 243, 254, 0.08);\n", + " --border-medium: rgba(225, 243, 254, 0.12);\n", + "\n", + " /* Warm gradients */\n", + " --gradient-warm: linear-gradient(\n", + " 135deg,\n", + " rgba(232, 192, 170, 0.05),\n", + " rgba(236, 98, 92, 0.05)\n", + " );\n", + " }\n", + "\n", + " body.Document {\n", + " background-color: var(--bg-primary);\n", + " color: var(--text-primary);\n", + " font-family: Arial, sans-serif;\n", + " line-height: 1.6;\n", + " background-image: linear-gradient(\n", + " 170deg,\n", + " var(--overlay-blue),\n", + " var(--overlay-teal)\n", + " );\n", + " min-height: 100vh;\n", + " }\n", + "\n", + " .Section, .Page, .Column {\n", + " margin-bottom: 20px;\n", + " }\n", + "\n", + " .Page {\n", + " margin-bottom: 40px;\n", + " padding: 30px;\n", + " background-color: var(--bg-secondary);\n", + " border-radius: 12px;\n", + " box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\n", + " position: relative;\n", + " border: 1px solid var(--accent-teal);\n", + " backdrop-filter: blur(20px);\n", + " }\n", + "\n", + " .Page::after {\n", + " content: attr(data-page-number);\n", + " position: absolute;\n", + " bottom: 15px;\n", + " right: 15px;\n", + " font-size: 0.8em;\n", + " color: var(--accent-teal);\n", + " background: linear-gradient(135deg, var(--bg-secondary), var(--bg-tertiary));\n", + " padding: 4px 12px;\n", + " border-radius: 6px;\n", + " border: 1px solid var(--border-light);\n", + " }\n", + "\n", + " .Page:not(:last-child)::before {\n", + " content: '';\n", + " display: block;\n", + " height: 1px;\n", + " background-color: var(--border-medium);\n", + " margin: 15px 0;\n", + " }\n", + "\n", + " .Section {\n", + " background: linear-gradient(\n", + " 170deg,\n", + " var(--bg-secondary),\n", + " var(--bg-tertiary)\n", + " );\n", + " border-radius: 12px;\n", + " padding: 10px;\n", + " margin-bottom: 30px;\n", + " border: 1px solid var(--accent-pink);\n", + " display: flex;\n", + " flex-wrap: wrap;\n", + " justify-content: space-between;\n", + " }\n", + "\n", + " .Section.featured {\n", + " background: var(--gradient-warm);\n", + " border: 1px solid var(--accent-red);\n", + " }\n", + "\n", + " .Column {\n", + " flex: 1;\n", + " min-width: 0;\n", + " padding: 0 10px;\n", + " }\n", + "\n", + " .Section.two-column .Column {\n", + " flex-basis: calc(50% - 20px);\n", + " }\n", + "\n", + " .Section.three-column .Column {\n", + " flex-basis: calc(33.333% - 20px);\n", + " }\n", + "\n", + " .Section.four-column .Column {\n", + " flex-basis: calc(25% - 20px);\n", + " }\n", + "\n", + " .Section.five-column .Column {\n", + " flex-basis: calc(20% - 20px);\n", + " }\n", + "\n", + " .Header, .Footer, .Sidebar {\n", + " padding: 10px;\n", + " border-radius: 5px;\n", + " }\n", + "\n", + " .PageBreak {\n", + " border: none;\n", + " border-top: 1px dashed var(--border-medium);\n", + " margin: 20px 0;\n", + " }\n", + "\n", + " .Title {\n", + " color: var(--accent-teal);\n", + " margin-bottom: 20px;\n", + " font-size: 2em;\n", + " letter-spacing: -0.02em;\n", + " border-radius: 10px;\n", + " background: var(--gradient-warm);\n", + " }\n", + "\n", + " .Subtitle {\n", + " color: var(--accent-red);\n", + " margin-bottom: 15px;\n", + " font-size: 1.2em;\n", + " letter-spacing: 0.01em;\n", + " }\n", + "\n", + " .Heading {\n", + " color: var(--accent-blue);\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .NarrativeText, .UncategorizedText {\n", + " margin-bottom: 10px;\n", + " color: grey;\n", + " }\n", + "\n", + " .Quote {\n", + " border-left: 3px solid var(--accent-lime);\n", + " padding: 20px;\n", + " margin: 20px 0;\n", + " background: linear-gradient(90deg,\n", + " rgba(139, 250, 173, 0.08),\n", + " rgba(232, 192, 170, 0.03)\n", + " );\n", + " border-radius: 0 8px 8px 0;\n", + " }\n", + "\n", + " .Footnote, .Caption, .PageNumber {\n", + " font-size: 0.9em;\n", + " color: var(--text-muted);\n", + " }\n", + " .OrderedList, .UnorderedList, .DefinitionList {\n", + " margin-left: 20px;\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .ListItem {\n", + " margin-bottom: 5px;\n", + " list-style-type: circle;\n", + " color: lightgrey;\n", + " }\n", + "\n", + " .Table {\n", + " border-collapse: separate;\n", + " border-spacing: 0;\n", + " width: 100%;\n", + " border-radius: 8px;\n", + " overflow: hidden;\n", + " border: 1px solid var(--accent-peach);\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .TableRow, .TableCell {\n", + " border: 1px solid var(--accent-peach);\n", + " padding: 12px 16px;\n", + " }\n", + "\n", + " .TableHeader {\n", + " background: linear-gradient(90deg, var(--bg-tertiary), var(--bg-secondary));\n", + " color: var(--accent-lime);\n", + " font-weight: 500;\n", + " padding: 12px 16px;\n", + " }\n", + "\n", + " .Image, .Figure, .Video, .Audio, .Barcode, .QRCode, .Logo {\n", + " height: auto;\n", + " margin-bottom: 5px;\n", + " font-style: italic;\n", + " }\n", + "\n", + " .CodeBlock, .InlineCode {\n", + " font-family: 'Fira Code', monospace;\n", + " background: var(--bg-tertiary);\n", + " padding: 20px;\n", + " border-radius: 8px;\n", + " border: 1px solid var(--border-medium);\n", + " color: var(--accent-lime);\n", + " margin: 20px 0;\n", + " }\n", + "\n", + " .Formula, .Equation {\n", + " font-style: italic;\n", + " margin: 10px 0;\n", + " }\n", + "\n", + " .FootnoteReference, .Citation, .Bibliography, .Glossary {\n", + " color: var(--accent-teal);\n", + " }\n", + "\n", + " .Author, .Date, .Keywords {\n", + " color: var(--text-secondary);\n", + " font-size: 0.9em;\n", + " }\n", + "\n", + " .TableOfContents, .Index {\n", + " background: var(--bg-tertiary);\n", + " padding: 20px;\n", + " border-radius: 8px;\n", + " margin-bottom: 15px;\n", + " border: 1px solid var(--border-medium);\n", + " }\n", + "\n", + " .Form {\n", + " margin-bottom: 15px;\n", + " }\n", + "\n", + " .FormField {\n", + " background: var(--bg-tertiary);\n", + " border: 1px solid var(--border-medium);\n", + " padding: 12px;\n", + " border-radius: 6px;\n", + " color: var(--text-primary);\n", + " transition: all 0.2s ease;\n", + " display: block;\n", + " margin-bottom: 10px;\n", + " }\n", + "\n", + " .FormField:focus {\n", + " border-color: var(--accent-blue);\n", + " box-shadow: 0 0 0 3px rgba(104, 187, 243, 0.1);\n", + " }\n", + "\n", + " .FormFieldValue {\n", + " padding: 0px 8px 0px 8px;\n", + " color: grey;\n", + " border-radius: 9px;\n", + " border: 1px solid var(--border-medium);\n", + " }\n", + "\n", + " .Button {\n", + " background: linear-gradient(135deg, var(--accent-blue), var(--accent-teal));\n", + " color: var(--bg-primary);\n", + " border: none;\n", + " padding: 10px 20px;\n", + " border-radius: 6px;\n", + " cursor: pointer;\n", + " font-weight: 500;\n", + " transition: all 0.3s ease;\n", + " box-shadow: 0 2px 8px rgba(104, 187, 243, 0.2);\n", + " }\n", + "\n", + " .Button:hover {\n", + " transform: translateY(-1px);\n", + " box-shadow: 0 4px 12px rgba(104, 187, 243, 0.3);\n", + " }\n", + "\n", + " .Button.critical {\n", + " background: linear-gradient(135deg,\n", + " var(--accent-red),\n", + " var(--accent-pink-red)\n", + " );\n", + " }\n", + "\n", + " .Comment {\n", + " color: var(--accent-lime);\n", + " font-style: italic;\n", + " }\n", + "\n", + " .Highlight {\n", + " background: linear-gradient(120deg,\n", + " rgba(253, 246, 128, 0.1),\n", + " rgba(232, 167, 225, 0.1)\n", + " );\n", + " padding: 2px 6px;\n", + " border-radius: 4px;\n", + " }\n", + "\n", + " .RevisionInsertion {\n", + " color: var(--accent-lime);\n", + " text-decoration: none;\n", + " border-bottom: 1px dashed rgba(139, 250, 173, 0.4);\n", + " }\n", + "\n", + " .RevisionDeletion {\n", + " color: var(--accent-red);\n", + " text-decoration: line-through;\n", + " border-bottom: 1px dashed rgba(236, 98, 92, 0.4);\n", + " }\n", + "\n", + " .Address, .EmailAddress, .PhoneNumber, .Date, .Time, .Currency, .Measurement {\n", + " color: var(--accent-peach);\n", + " }\n", + "\n", + " .Date.important {\n", + " color: var(--accent-pink-red);\n", + " font-weight: 500;\n", + " }\n", + "\n", + " .Letterhead, .Signature, .Watermark, .Stamp {\n", + " opacity: 0.7;\n", + " }\n", + "\n", + " .Hyperlink {\n", + " color: var(--accent-teal);\n", + " text-decoration: none;\n", + " transition: all 0.2s ease;\n", + " border-bottom: 1px solid rgba(101, 218, 244, 0.2);\n", + " }\n", + "\n", + " .Hyperlink:hover {\n", + " border-bottom-color: var(--accent-teal);\n", + " }\n", + "\n", + " .Hyperlink.featured {\n", + " background: linear-gradient(\n", + " 90deg,\n", + " var(--accent-peach),\n", + " var(--accent-pink)\n", + " );\n", + " -webkit-text-fill-color: transparent;\n", + " border-bottom: none;\n", + " }\n", + "\n", + " .Alert {\n", + " background: linear-gradient(\n", + " 135deg,\n", + " rgba(236, 98, 92, 0.08),\n", + " rgba(232, 167, 225, 0.05)\n", + " );\n", + " border-left: 3px solid var(--accent-red);\n", + " padding: 16px 20px;\n", + " border-radius: 0 8px 8px 0;\n", + " margin: 20px 0;\n", + " }\n", + "\n", + " .Priority {\n", + " display: inline-block;\n", + " padding: 2px 8px;\n", + " border-radius: 4px;\n", + " font-size: 0.85em;\n", + " font-weight: 500;\n", + " }\n", + "\n", + " .Priority.high {\n", + " background: rgba(236, 98, 92, 0.1);\n", + " color: var(--accent-red);\n", + " border: 1px solid rgba(236, 98, 92, 0.2);\n", + " }\n", + "\n", + " .Priority.medium {\n", + " background: rgba(232, 167, 225, 0.1);\n", + " color: var(--accent-pink);\n", + " border: 1px solid rgba(232, 167, 225, 0.2);\n", + " }\n", + "\n", + " .Tag {\n", + " display: inline-block;\n", + " padding: 3px 10px;\n", + " border-radius: 12px;\n", + " font-size: 0.85em;\n", + " margin: 0 4px;\n", + " background: var(--bg-tertiary);\n", + " }\n", + "\n", + " .Tag.featured {\n", + " background: rgba(232, 192, 170, 0.1);\n", + " color: var(--accent-peach);\n", + " border: 1px solid rgba(232, 192, 170, 0.15);\n", + " }\n", + "\n", + " .Progress {\n", + " height: 4px;\n", + " background: var(--bg-tertiary);\n", + " border-radius: 2px;\n", + " overflow: hidden;\n", + " }\n", + "\n", + " .Progress .bar {\n", + " height: 100%;\n", + " background: linear-gradient(\n", + " 90deg,\n", + " var(--accent-blue),\n", + " var(--accent-pink)\n", + " );\n", + " transition: width 0.3s ease;\n", + " }\n", + "\n", + " .Decorator {\n", + " position: absolute;\n", + " width: 100px;\n", + " height: 100px;\n", + " opacity: 0.03;\n", + " filter: blur(40px);\n", + " border-radius: 50%;\n", + " pointer-events: none;\n", + " }\n", + "\n", + " ::selection {\n", + " background: rgba(232, 167, 225, 0.2);\n", + " color: var(--text-primary);\n", + " }\n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ueCVNGR8ObMm" + }, + "outputs": [], + "source": [ + "# Extension methods for styling\n", + "def apply_light_theme(self) -> 'VlmJsonToHtmlConverter':\n", + " \"\"\"Apply light theme styling from CSS string.\"\"\"\n", + " self._theme_css = LIGHT_MODE_CSS_STYLING\n", + " return self\n", + "\n", + "def apply_dark_theme(self) -> 'VlmJsonToHtmlConverter':\n", + " \"\"\"Apply dark theme styling from CSS string.\"\"\"\n", + " self._theme_css = DARK_MODE_CSS_STYLING\n", + " return self\n", + "\n", + "def apply_custom_css_styling(self, css_content: str) -> 'VlmJsonToHtmlConverter':\n", + " \"\"\"Apply custom CSS content.\"\"\"\n", + " self._theme_css = css_content\n", + " return self\n", + "\n", + "# Add the styling methods to JSON2HTMLConverter\n", + "VlmJsonToHtmlConverter.apply_light_theme = apply_light_theme\n", + "VlmJsonToHtmlConverter.apply_dark_theme = apply_dark_theme\n", + "VlmJsonToHtmlConverter.apply_custom_css_styling = apply_custom_css_styling" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1HBbkczhS9SM" + }, + "outputs": [], + "source": [ + "file_as_stylized_html = VlmJsonToHtmlConverter().apply_light_theme().convert(file_as_json)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hqt4TZCeUA4f" + }, + "source": [ + "#### Download the stylized file to view it in a browser!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qIGzqByITRa2" + }, + "outputs": [], + "source": [ + "print(file_as_stylized_html)\n", + "stylized_html_file_name = f\"{filename}-stylized.html\"\n", + "with open(stylized_html_file_name, \"w\") as file:\n", + " file.write(file_as_stylized_html)\n", + "\n", + "if GOOGLE_COLAB:\n", + " google_colab_files.download(stylized_html_file_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next Steps\n", + "Now that you've learned how to extract and stylize the HTML representation of your documents, you're one step closer to being able to deploy this capability into your production applications! \n", + "\n", + "\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "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.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}