{ "cells": [ { "cell_type": "markdown", "id": "35bc0aa2-8dc1-4d4b-acee-29137db5f78b", "metadata": {}, "source": [ "# fixing the order of pages md OCR merge right and left + remove the page number " ] }, { "cell_type": "code", "execution_count": 3, "id": "3c1f10a8-1aea-4de2-b70b-ecb98cde44c8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "11to20.md — 10 pages → 8 pages (2 pair(s) merged)\n", "1to10.md — 10 pages → 9 pages (1 pair(s) merged)\n", "\n", "✅ Done! Files saved to: /home/skiredj.abderrahman/khalil/Benchmarking_OCR/tests/output\n" ] } ], "source": [ "\n", "import os\n", "import re\n", "from pathlib import Path\n", "\n", "# ── Change these two paths ──────────────────────────────────────────\n", "INPUT_FOLDER = \"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/tests/input\"\n", "OUTPUT_FOLDER = \"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/tests/output\"\n", "# ────────────────────────────────────────────────────────────────────\n", "\n", "def parse_pages(content):\n", " pattern = r'(=== Page (\\d+) \\(([^)]+)\\) ===)'\n", " parts = re.split(pattern, content)\n", " \n", " preamble = parts[0]\n", " pages = []\n", " i = 1\n", " while i < len(parts):\n", " pages.append({\n", " 'header': parts[i],\n", " 'num': int(parts[i+1]),\n", " 'id': parts[i+2],\n", " 'body': parts[i+3]\n", " })\n", " i += 4\n", " return preamble, pages\n", "\n", "def merge_pages(pages):\n", " merged = []\n", " skip_ids = set()\n", " \n", " for i, page in enumerate(pages):\n", " pid = page['id']\n", " if pid in skip_ids:\n", " continue\n", " \n", " if pid.endswith('_right'):\n", " base_id = pid[:-len('_right')]\n", " left_page = None\n", " for j in range(i+1, len(pages)):\n", " if pages[j]['id'] == base_id + '_left':\n", " left_page = pages[j]\n", " skip_ids.add(pages[j]['id'])\n", " break\n", " \n", " combined_body = (page['body'].rstrip() + '\\n' + left_page['body']) if left_page else page['body']\n", " merged.append({'id': base_id, 'body': combined_body})\n", "\n", " elif pid.endswith('_left'):\n", " # Orphan left — strip suffix, keep content\n", " merged.append({'id': pid[:-len('_left')], 'body': page['body']})\n", " else:\n", " merged.append({'id': pid, 'body': page['body']})\n", " \n", " # Renumber sequentially\n", " return [{'num': n, 'id': p['id'], 'body': p['body']} for n, p in enumerate(merged, start=1)]\n", "\n", "def rebuild_content(preamble, pages):\n", " result = preamble\n", " for page in pages:\n", " result += f\"=== {page['id']} ===\\n\"\n", " result += page['body']\n", " return result\n", "\n", "def process_folder(input_folder, output_folder):\n", " input_path = Path(input_folder)\n", " output_path = Path(output_folder)\n", " output_path.mkdir(parents=True, exist_ok=True)\n", " \n", " md_files = list(input_path.glob('*.md'))\n", " if not md_files:\n", " print(f\"No .md files found in {input_folder}\")\n", " return\n", " \n", " for md_file in md_files:\n", " content = md_file.read_text(encoding='utf-8')\n", " preamble, pages = parse_pages(content)\n", " \n", " if not pages:\n", " (output_path / md_file.name).write_text(content, encoding='utf-8')\n", " print(f\"{md_file.name} — no pages found, copied as-is.\")\n", " continue\n", " \n", " merged = merge_pages(pages)\n", " output_path.joinpath(md_file.name).write_text(rebuild_content(preamble, merged), encoding='utf-8')\n", " \n", " print(f\"{md_file.name} — {len(pages)} pages → {len(merged)} pages ({len(pages)-len(merged)} pair(s) merged)\")\n", " \n", " print(f\"\\n✅ Done! Files saved to: {output_path.resolve()}\")\n", "\n", "process_folder(INPUT_FOLDER, OUTPUT_FOLDER)" ] }, { "cell_type": "markdown", "id": "fae56960-334b-45c1-b777-e8efe56d05ae", "metadata": {}, "source": [ "# OCR sans split " ] }, { "cell_type": "code", "execution_count": 3, "id": "87da963c-9081-44f3-9f14-6d1a4a399be8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "============================================================\n", " SCAN DU DATASET\n", "============================================================\n", " Dossier racine : /home/skiredj.abderrahman/khalil/Benchmarking_OCR/100images/100images\n", "\n", " 📁 single_column_text → 27 image(s)\n", " 📁 sparse_blank → 8 image(s)\n", " 📁 table_full → 10 image(s)\n", " 📁 table_with_text → 20 image(s)\n", " 📁 two_column_text → 35 image(s)\n", "\n", "────────────────────────────────────────────────────────────\n", " Sous-dossiers trouvés : 5\n", " Sous-dossiers avec PNG : 5\n", " Sous-dossiers vides : 0\n", " Total images PNG : 100\n", "────────────────────────────────────────────────────────────\n", " Sortie markdown : /home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\n", "============================================================\n", "\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "Sous-dossiers: 0%| | 0/5 [00:00 0 and lines[i - 1].startswith(\"=== Page\")\n", " prev_ends_sentence = i > 0 and lines[i - 1].rstrip().endswith(\".\")\n", " if not prev_is_page_header and not prev_ends_sentence and out and out[-1] != \"\":\n", " out[-1] = out[-1].rstrip() + \" \" + line.lstrip()\n", " else:\n", " out.append(line)\n", " return \"\\n\".join(out)\n", "\n", "\n", "# ========== PIPELINE PRINCIPAL ==========\n", "\n", "def process_subfolder(subfolder_path, subfolder_name):\n", " \"\"\"\n", " Traite un sous-dossier : OCR direct sur les PNG → .md\n", " \"\"\"\n", " print(f\"\\n{'='*60}\")\n", " print(f\"Traitement : {subfolder_name}\")\n", " print(f\"{'='*60}\")\n", "\n", " txt_dir = os.path.join(TXT_TEMP_FOLDER, subfolder_name)\n", " output_md = os.path.join(OUTPUT_FOLDER, subfolder_name + \".md\")\n", " os.makedirs(txt_dir, exist_ok=True)\n", " os.makedirs(OUTPUT_FOLDER, exist_ok=True)\n", "\n", " if os.path.exists(output_md):\n", " print(f\" → Déjà traité, on passe.\")\n", " return\n", "\n", " # ── Étape 1 : Récupération et tri des images ───────────────────────────────\n", " png_files = sorted(\n", " glob.glob(os.path.join(subfolder_path, \"*.png\")),\n", " key=arabic_sort_key\n", " )\n", " if not png_files:\n", " print(f\" ⚠️ Aucune image PNG trouvée dans {subfolder_path}\")\n", " return\n", "\n", " # ── Étape 2 : OCR en parallèle ────────────────────────────────────────────\n", " tasks = [\n", " (img_p, os.path.join(txt_dir, os.path.splitext(os.path.basename(img_p))[0] + \".txt\"))\n", " for img_p in png_files\n", " ]\n", "\n", " print(f\" {len(png_files)} image(s) à envoyer à Chandra-2...\")\n", " with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor:\n", " futures = {executor.submit(process_image, ip, op): (ip, op) for ip, op in tasks}\n", " for future in tqdm(as_completed(futures), total=len(futures),\n", " desc=\" OCR\", leave=False):\n", " try:\n", " future.result()\n", " except Exception as e:\n", " print(f\" ❌ Erreur non gérée : {e}\")\n", "\n", " # ── Étape 3 : Assemblage en Markdown ──────────────────────────────────────\n", " parts = []\n", " for i, img_p in enumerate(png_files, 1):\n", " img_name = os.path.splitext(os.path.basename(img_p))[0]\n", " txt_path = os.path.join(txt_dir, img_name + \".txt\")\n", " if os.path.exists(txt_path):\n", " content = open(txt_path, encoding=\"utf-8\").read()\n", " else:\n", " content = \"[OCR failed for this page]\"\n", " parts.append(f\"=== Page {i} ({img_name}) ===\\n{content}\\n\")\n", "\n", " final = clean_unnecessary_linebreaks(\"\\n\".join(parts))\n", " with open(output_md, \"w\", encoding=\"utf-8\") as f:\n", " f.write(final)\n", "\n", " print(f\" ✅ Markdown sauvegardé → {output_md}\")\n", "\n", "\n", "def run_pipeline():\n", " \"\"\"\n", " Point d'entrée principal.\n", " Parcourt tous les sous-dossiers de IMAGES_DATASET_FOLDER,\n", " affiche un récapitulatif complet, puis lance le traitement.\n", " \"\"\"\n", " subfolders = sorted([\n", " d for d in os.listdir(IMAGES_DATASET_FOLDER)\n", " if os.path.isdir(os.path.join(IMAGES_DATASET_FOLDER, d))\n", " ])\n", "\n", " if not subfolders:\n", " print(f\"Aucun sous-dossier trouvé dans {IMAGES_DATASET_FOLDER}\")\n", " return\n", "\n", " # ── Scan préalable ────────────────────────────────────────────────────────\n", " print(f\"\\n{'='*60}\")\n", " print(f\" SCAN DU DATASET\")\n", " print(f\"{'='*60}\")\n", " print(f\" Dossier racine : {IMAGES_DATASET_FOLDER}\\n\")\n", "\n", " folder_image_counts = {}\n", " max_name_len = max(len(d) for d in subfolders)\n", "\n", " for name in subfolders:\n", " path = os.path.join(IMAGES_DATASET_FOLDER, name)\n", " pngs = sorted(glob.glob(os.path.join(path, \"*.png\")))\n", " folder_image_counts[name] = pngs\n", " status = f\"{len(pngs):>4} image(s)\" if pngs else \" ── aucune image PNG\"\n", " print(f\" 📁 {name:<{max_name_len}} → {status}\")\n", "\n", " total_images = sum(len(v) for v in folder_image_counts.values())\n", " total_folders = len(subfolders)\n", " empty_folders = sum(1 for v in folder_image_counts.values() if not v)\n", " active_folders = total_folders - empty_folders\n", "\n", " print(f\"\\n{'─'*60}\")\n", " print(f\" Sous-dossiers trouvés : {total_folders}\")\n", " print(f\" Sous-dossiers avec PNG : {active_folders}\")\n", " print(f\" Sous-dossiers vides : {empty_folders}\")\n", " print(f\" Total images PNG : {total_images}\")\n", " print(f\"{'─'*60}\")\n", " print(f\" Sortie markdown : {OUTPUT_FOLDER}\")\n", " print(f\"{'='*60}\\n\")\n", "\n", " if total_images == 0:\n", " print(\"Aucune image à traiter. Arrêt.\")\n", " return\n", "\n", " # ── Traitement ────────────────────────────────────────────────────────────\n", " for name in tqdm(subfolders, desc=\"Sous-dossiers\"):\n", " if not folder_image_counts[name]:\n", " continue\n", " path = os.path.join(IMAGES_DATASET_FOLDER, name)\n", " try:\n", " process_subfolder(path, name)\n", " except Exception as e:\n", " print(f\"❌ Erreur sur {name} : {e}\")\n", "\n", " print(f\"\\n{'='*60}\")\n", " print(\"PIPELINE TERMINÉ\")\n", " print(f\" {active_folders} sous-dossier(s) traité(s), {total_images} image(s) au total\")\n", " print(f\" Résultats dans : {OUTPUT_FOLDER}\")\n", " print(f\"{'='*60}\")\n", "\n", "\n", "# ========== LANCEMENT ==========\n", "\n", "if __name__ == \"__main__\":\n", " run_pipeline()" ] }, { "cell_type": "markdown", "id": "831e648e-ee04-4b48-8975-89983e4e2973", "metadata": {}, "source": [ "# html to md " ] }, { "cell_type": "code", "execution_count": 6, "id": "dd5a9744-21e3-4ff1-9821-99d6fa6f5bc8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Found 5 files\n", "Processing in batches of 25 with 10s wait\n", "\n", "Processing: single_column_text.md\n", "✓ Saved: single_column_text.md\n", "Processing: sparse_blank.md\n", "✓ Saved: sparse_blank.md\n", "Processing: table_full.md\n", "✓ Saved: table_full.md\n", "Processing: table_with_text.md\n", "✓ Saved: table_with_text.md\n", "Processing: two_column_text.md\n", "✓ Saved: two_column_text.md\n", "\n", "============================================================\n", "✓ Complete! Converted: 5 | Failed: 0 | Total: 5\n" ] } ], "source": [ "# exectued tomd.pbs\n", "from pathlib import Path\n", "from markdownify import markdownify as md\n", "import re\n", "import time\n", "\n", "def convert_markdown_file(file_path, output_folder=None):\n", " \"\"\"Convert HTML in markdown file to proper markdown.\"\"\"\n", " print(f\"Processing: {file_path.name}\")\n", "\n", " # Read the file\n", " content = file_path.read_text(encoding='utf-8')\n", "\n", " # Split by page sections\n", " page_pattern = r'(=== Page \\d+ \\([^)]+\\) ===)'\n", " sections = re.split(page_pattern, content)\n", "\n", " # Convert HTML sections to markdown\n", " converted = []\n", " for section in sections:\n", " if section.startswith('=== Page'):\n", " converted.append(section)\n", " elif section.strip():\n", " # Convert HTML to markdown\n", " markdown = md(section, heading_style=\"ATX\")\n", " converted.append('\\n' + markdown.strip() + '\\n')\n", "\n", " # Save the result\n", " output_path = output_folder / file_path.name if output_folder else file_path.parent / f\"{file_path.stem}_converted.md\"\n", " output_path.write_text('\\n'.join(converted), encoding='utf-8')\n", "\n", " print(f\"✓ Saved: {output_path.name}\")\n", "\n", "def convert_all_files(input_folder, output_folder=None, batch_size=50, wait_seconds=5):\n", " \"\"\"Convert all markdown files in folder with batching.\"\"\"\n", " input_path = Path(input_folder)\n", " output_path = Path(output_folder) if output_folder else None\n", "\n", " if output_path:\n", " output_path.mkdir(parents=True, exist_ok=True)\n", "\n", " md_files = list(input_path.glob('*.md'))\n", " total_files = len(md_files)\n", " print(f\"Found {total_files} files\")\n", " print(f\"Processing in batches of {batch_size} with {wait_seconds}s wait\\n\")\n", "\n", " converted = 0\n", " failed = 0\n", "\n", " for i, md_file in enumerate(md_files, 1):\n", " try:\n", " convert_markdown_file(md_file, output_path)\n", " converted += 1\n", " except Exception as e:\n", " print(f\"✗ Error: {md_file.name} - {e}\")\n", " failed += 1\n", "\n", " # Batch wait\n", " if i % batch_size == 0 and i < total_files:\n", " print(f\"\\n--- Processed {i}/{total_files} files ---\")\n", " print(f\"Waiting {wait_seconds} seconds...\\n\")\n", " time.sleep(wait_seconds)\n", "\n", " print(f\"\\n{'='*60}\")\n", " print(f\"✓ Complete! Converted: {converted} | Failed: {failed} | Total: {total_files}\")\n", "\n", "if __name__ == \"__main__\":\n", " input_folder = r\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\"\n", " output_folder = r\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\"\n", "\n", " convert_all_files(input_folder, output_folder, batch_size=25, wait_seconds=10)\n", "\n", " # Option 2: Save to new folder with custom batch settings (uncomment)\n", " # output_folder = r\"C:\\...\\converted_markdowns\"\n", " # convert_all_files(input_folder, output_folder, batch_size=100, wait_seconds=3)\n" ] }, { "cell_type": "markdown", "id": "06e0d36d-5bc0-4675-8e5d-9aa088fcbc79", "metadata": {}, "source": [ "# no page number " ] }, { "cell_type": "code", "execution_count": 7, "id": "cb919ce7-acc5-4e09-b3ce-6250f19aed44", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "single_column_text.md — 27 pages → 27 pages (0 pair(s) merged)\n", "sparse_blank.md — 8 pages → 8 pages (0 pair(s) merged)\n", "table_full.md — 10 pages → 10 pages (0 pair(s) merged)\n", "table_with_text.md — 20 pages → 20 pages (0 pair(s) merged)\n", "two_column_text.md — 35 pages → 35 pages (0 pair(s) merged)\n", "\n", "✅ Done! Files saved to: /home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\n" ] } ], "source": [ "\n", "import os\n", "import re\n", "from pathlib import Path\n", "\n", "# ── Change these two paths ──────────────────────────────────────────\n", "INPUT_FOLDER = r\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\"\n", "OUTPUT_FOLDER = r\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\"\n", "# ────────────────────────────────────────────────────────────────────\n", "\n", "def parse_pages(content):\n", " pattern = r'(=== Page (\\d+) \\(([^)]+)\\) ===)'\n", " parts = re.split(pattern, content)\n", " \n", " preamble = parts[0]\n", " pages = []\n", " i = 1\n", " while i < len(parts):\n", " pages.append({\n", " 'header': parts[i],\n", " 'num': int(parts[i+1]),\n", " 'id': parts[i+2],\n", " 'body': parts[i+3]\n", " })\n", " i += 4\n", " return preamble, pages\n", "\n", "def merge_pages(pages):\n", " merged = []\n", " skip_ids = set()\n", " \n", " for i, page in enumerate(pages):\n", " pid = page['id']\n", " if pid in skip_ids:\n", " continue\n", " \n", " if pid.endswith('_right'):\n", " base_id = pid[:-len('_right')]\n", " left_page = None\n", " for j in range(i+1, len(pages)):\n", " if pages[j]['id'] == base_id + '_left':\n", " left_page = pages[j]\n", " skip_ids.add(pages[j]['id'])\n", " break\n", " \n", " combined_body = (page['body'].rstrip() + '\\n' + left_page['body']) if left_page else page['body']\n", " merged.append({'id': base_id, 'body': combined_body})\n", "\n", " elif pid.endswith('_left'):\n", " # Orphan left — strip suffix, keep content\n", " merged.append({'id': pid[:-len('_left')], 'body': page['body']})\n", " else:\n", " merged.append({'id': pid, 'body': page['body']})\n", " \n", " # Renumber sequentially\n", " return [{'num': n, 'id': p['id'], 'body': p['body']} for n, p in enumerate(merged, start=1)]\n", "\n", "def rebuild_content(preamble, pages):\n", " result = preamble\n", " for page in pages:\n", " result += f\"=== {page['id']} ===\\n\"\n", " result += page['body']\n", " return result\n", "\n", "def process_folder(input_folder, output_folder):\n", " input_path = Path(input_folder)\n", " output_path = Path(output_folder)\n", " output_path.mkdir(parents=True, exist_ok=True)\n", " \n", " md_files = list(input_path.glob('*.md'))\n", " if not md_files:\n", " print(f\"No .md files found in {input_folder}\")\n", " return\n", " \n", " for md_file in md_files:\n", " content = md_file.read_text(encoding='utf-8')\n", " preamble, pages = parse_pages(content)\n", " \n", " if not pages:\n", " (output_path / md_file.name).write_text(content, encoding='utf-8')\n", " print(f\"{md_file.name} — no pages found, copied as-is.\")\n", " continue\n", " \n", " merged = merge_pages(pages)\n", " output_path.joinpath(md_file.name).write_text(rebuild_content(preamble, merged), encoding='utf-8')\n", " \n", " print(f\"{md_file.name} — {len(pages)} pages → {len(merged)} pages ({len(pages)-len(merged)} pair(s) merged)\")\n", " \n", " print(f\"\\n✅ Done! Files saved to: {output_path.resolve()}\")\n", "\n", "process_folder(INPUT_FOLDER, OUTPUT_FOLDER)" ] }, { "cell_type": "markdown", "id": "f8b7a93c-7b7f-48f7-9afa-4ef1eb1dba91", "metadata": {}, "source": [ "# keep only l 3adad + jarida after page number " ] }, { "cell_type": "code", "execution_count": 8, "id": "d3e2b2af-815b-49b9-9ee8-e0fd921343ad", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Found 5 .md files\n", "\n", "✓ single_column_text.md\n", "✓ sparse_blank.md\n", "✓ table_full.md\n", "✓ table_with_text.md\n", "✓ two_column_text.md\n", "\n", "Done — cleaned files saved to: /home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown_clean\n" ] } ], "source": [ "import re\n", "from pathlib import Path\n", "\n", "# ── CONFIG ──────────────────────────────────────────────────────────────────\n", "INPUT_FOLDER = Path(\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown\") # 👈 change this\n", "OUTPUT_FOLDER = Path(\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/100_images_sans_split/output_markdown_clean\") # 👈 change this\n", "OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)\n", "\n", "# ── PATTERNS ────────────────────────────────────────────────────────────────\n", "PAGE_MARKER = re.compile(r'(=== BO_\\w+ ===)')\n", "DATE_LINE = re.compile(r'(عدد\\s+\\d+\\s*-[^\\n]+)')\n", "JARIDA = 'الجريدة الرسمية'\n", "\n", "# ── PROCESS ─────────────────────────────────────────────────────────────────\n", "def clean_md(text: str) -> str:\n", " # Split into page blocks on the === ... === markers\n", " # re.split with a capture group keeps the delimiters\n", " parts = PAGE_MARKER.split(text)\n", " # parts = [pre_text, marker1, block1, marker2, block2, ...]\n", " \n", " output_lines = []\n", " \n", " i = 0\n", " # Skip any content before the first page marker\n", " if parts and not PAGE_MARKER.match(parts[0].strip()):\n", " i = 1 # skip preamble\n", " \n", " while i < len(parts):\n", " chunk = parts[i]\n", " \n", " if PAGE_MARKER.match(chunk.strip()):\n", " # This is a page marker — always keep it\n", " output_lines.append(chunk.strip())\n", " i += 1\n", " \n", " # Next chunk is the body of this page\n", " if i < len(parts):\n", " body = parts[i]\n", " \n", " # Extract date line (first match)\n", " date_match = DATE_LINE.search(body)\n", " if date_match:\n", " output_lines.append(date_match.group(1).strip())\n", " \n", " # Add الجريدة الرسمية once\n", " output_lines.append(JARIDA)\n", " \n", " i += 1\n", " else:\n", " i += 1 # skip unexpected chunk\n", " \n", " return '\\n'.join(output_lines) + '\\n'\n", "\n", "\n", "# ── RUN ──────────────────────────────────────────────────────────────────────\n", "md_files = list(INPUT_FOLDER.glob(\"*.md\"))\n", "print(f\"Found {len(md_files)} .md files\\n\")\n", "\n", "for fp in md_files:\n", " text = fp.read_text(encoding=\"utf-8\")\n", " cleaned = clean_md(text)\n", " out_fp = OUTPUT_FOLDER / fp.name\n", " out_fp.write_text(cleaned, encoding=\"utf-8\")\n", " print(f\"✓ {fp.name}\")\n", "\n", "print(f\"\\nDone — cleaned files saved to: {OUTPUT_FOLDER.resolve()}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "24a7a55b-0d31-410f-8b1a-260d63e3e349", "metadata": {}, "outputs": [], "source": [] } ], "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.20" } }, "nbformat": 4, "nbformat_minor": 5 }