{ "cells": [ { "cell_type": "markdown", "id": "bcf8bfe6-3c3f-42d6-b5fe-c46afed62587", "metadata": {}, "source": [ "# code de normalisation pour evaluer les 8 metrics d accuracy du text " ] }, { "cell_type": "code", "execution_count": 1, "id": "60277596-11a4-436e-858d-9f8c06a69d50", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "✅ 6/6 tests passés\n" ] } ], "source": [ "import re\n", "import unicodedata\n", "\n", "\n", "def normalize_arabic_ocr(text: str, remove_markdown: bool = False) -> str:\n", " \"\"\"\n", " Pipeline de normalisation canonique pour benchmark OCR arabe.\n", " À appliquer de manière IDENTIQUE sur GT et hypothèse OCR.\n", "\n", " Étapes :\n", " 1. (Optionnel) Suppression Markdown\n", " 2. NFKC — formes de présentation arabes\n", " 3. Caractères invisibles (ZWJ, ZWNJ, marques bidi, BOM)\n", " 4. Diacritiques arabes (tashkil)\n", " 5. Tatweel / kashida\n", " 6. Alef variants → ا\n", " 7. Alef maqsura → ي ← nouveau\n", " 8. Ta marbuta → ه\n", " 9. Hamza sur support : ؤ→و ئ→ي\n", " 10. Ponctuation arabe + latine → espace\n", " 11. Whitelist : garde arabe + chiffres + latin + espace ← nouveau\n", " 12. Chiffres arabes-indiens → ASCII\n", " 13. Latin → minuscules ← nouveau\n", " 14. Espaces multiples → un seul, strip\n", " \"\"\"\n", " if not isinstance(text, str) or not text.strip():\n", " return \"\"\n", "\n", " # ── 1. Markdown (désactivé par défaut pour OCR pur) ──────────────────────\n", " if remove_markdown:\n", " text = re.sub(r'#{1,6}\\s*', '', text)\n", " text = re.sub(r'^\\s*[-*+]\\s+', '', text, flags=re.MULTILINE)\n", " text = re.sub(r'\\*{1,3}(.*?)\\*{1,3}', r'\\1', text, flags=re.DOTALL)\n", " text = re.sub(r'`{1,3}.*?`{1,3}', ' ', text, flags=re.DOTALL)\n", " text = re.sub(r'\\[([^\\]]*)\\]\\([^\\)]*\\)', r'\\1', text)\n", " text = re.sub(r'!\\[[^\\]]*\\]\\([^\\)]*\\)', ' ', text)\n", " text = re.sub(r'\\|', ' ', text)\n", "\n", " # ── 2. NFKC ──────────────────────────────────────────────────────────────\n", " # NFC ne suffit pas : NFKC décompose aussi les formes de présentation\n", " # arabes (Presentation Forms-A/B : ﻛ ﻜ ﻟ ﻻ …) très fréquentes en OCR.\n", " text = unicodedata.normalize('NFKC', text)\n", "\n", " # ── 3. Caractères invisibles & marques bidirectionnelles ─────────────────\n", " text = re.sub(\n", " r'[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\uFEFF\\u00AD]',\n", " '', text\n", " )\n", "\n", " # ── 4. Diacritiques arabes (tashkil) ─────────────────────────────────────\n", " text = re.sub(\n", " r'[\\u064B-\\u065F' # fatha, damma, kasra, shadda, sukun …\n", " r'\\u0610-\\u061A' # Arabic extended marks\n", " r'\\u06D6-\\u06DC'\n", " r'\\u06DF-\\u06E4'\n", " r'\\u06E7\\u06E8'\n", " r'\\u06EA-\\u06ED]',\n", " '', text\n", " )\n", "\n", " # ── 5. Tatweel / kashida ─────────────────────────────────────────────────\n", " text = text.replace('\\u0640', '')\n", "\n", " # ── 6. Alef variants → ا ─────────────────────────────────────────────────\n", " text = re.sub(r'[\\u0622\\u0623\\u0625\\u0671\\u0672\\u0673\\u0675]', '\\u0627', text)\n", "\n", " # ── 7. Alef maqsura → ي ──────────────────────────────────────────────────\n", " text = text.replace('\\u0649', '\\u064A')\n", "\n", " # ── 8. Ta marbuta → ه ────────────────────────────────────────────────────\n", " text = text.replace('\\u0629', '\\u0647')\n", "\n", " # ── 9. Hamza sur support ─────────────────────────────────────────────────\n", " text = text.replace('\\u0624', '\\u0648') # ؤ → و\n", " text = text.replace('\\u0626', '\\u064A') # ئ → ي\n", "\n", " # ── 10. Ponctuation arabe + latine → espace ──────────────────────────────\n", " text = re.sub(r'[\\u060C\\u061B\\u061F\\u066A-\\u066D\\u0600-\\u0605]', ' ', text)\n", " text = re.sub(r'[.,:;()\\[\\]{}\\-«»\"\\'!?/\\\\~@#$%^&*_+=<>|]', ' ', text)\n", " \n", " # ── 13. Latin → minuscules ───────────────────────────────────────────────\n", " text = text.lower()\n", "\n", "\n", " # ── 11. Whitelist ─────────────────────────────────────────────────────────\n", " # Conserve : bloc arabe (0600-06FF + 0750-077F), chiffres ASCII,\n", " # latin a-z (noms propres, acronymes), espace.\n", " # Élimine : symboles math/monnaie, emojis, tirets typographiques,\n", " # guillemets exotiques, caractères de contrôle résiduels.\n", " text = re.sub(r'[^\\u0600-\\u06FF\\u0750-\\u077Fa-z0-9\\s]', ' ', text)\n", "\n", " # ── 12. Chiffres arabes-indiens → ASCII ──────────────────────────────────\n", " text = text.translate(str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789'))\n", "\n", " # ── 13. Latin → minuscules ───────────────────────────────────────────────\n", " text = text.lower()\n", "\n", " # ── 14. Espaces multiples → un seul ──────────────────────────────────────\n", " return re.sub(r'\\s+', ' ', text).strip()\n", "\n", "\n", "# ─── Vérification rapide ───────────────────────────────────────────────────\n", "if __name__ == '__main__':\n", " samples = [\n", " (\"Diacritiques\", \"الكِتَابُ المُفِيدُ\", \"الكتاب المفيد\"),\n", " (\"Alef maqsura\", \"موسى ويحيى\", \"موسي ويحيي\"),\n", " (\"Formes présent.\", \"ﻛﻠﻤﺔ\", \"كلمه\"),\n", " (\"Invisible chars\",\"م\\u200Bر\\u200Fحبا\", \"مرحبا\"),\n", " (\"Whitelist\", \"السعر: €15 أو 15€ !\", \"السعر 15 او 15\"),\n", " (\"Mixte latin\", \"نموذج GPT-4 وClaude\", \"نموذج gpt 4 وclaude\"),\n", " ]\n", " ok, fail = 0, 0\n", " for name, inp, expected in samples:\n", " result = normalize_arabic_ocr(inp)\n", " status = \"✅\" if result == expected else \"❌\"\n", " if result == expected:\n", " ok += 1\n", " else:\n", " fail += 1\n", " print(f\"{status} {name}: '{result}' ≠ '{expected}'\")\n", " print(f\"\\n{'✅' if fail == 0 else '⚠️'} {ok}/{ok+fail} tests passés\")" ] }, { "cell_type": "code", "execution_count": null, "id": "91743108-1eb3-47b7-a09e-bb3c048d7ee9", "metadata": {}, "outputs": [], "source": [ "import os\n", "from pathlib import Path\n", "\n", "input_folder = Path(\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/Ground_truth_first_20_not_normalised\")\n", "output_folder = Path(\"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/Ground_truth_first_20_normalised\")\n", "output_folder.mkdir(parents=True, exist_ok=True)\n", "\n", "for md_file in input_folder.glob(\"*.md\"):\n", " text = md_file.read_text(encoding=\"utf-8\")\n", " normalized = normalize_arabic_ocr(text, remove_markdown=True)\n", " out_path = output_folder / md_file.name\n", " out_path.write_text(normalized, encoding=\"utf-8\")\n", " print(f\"✅ {md_file.name}\")" ] }, { "cell_type": "markdown", "id": "041e5c05-eb98-4b85-a4ec-a7367458a005", "metadata": {}, "source": [ "# code benchmarking. evaluation des 8 metrics d accuracy + 1 metric TEDS pour les tableaux (9 metrics )" ] }, { "cell_type": "code", "execution_count": null, "id": "b4913d1b-5730-45dd-92d7-11c843fb014e", "metadata": {}, "outputs": [], "source": [ "import re\n", "import unicodedata\n", "import html as _html\n", "import json\n", "import editdistance\n", "import jiwer\n", "import sacrebleu\n", "from rouge_score import rouge_scorer as rs\n", "from apted import APTED, Config\n", "from lxml import etree\n", "from typing import Optional\n", "\n", "# ── Paths ─────────────────────────────────────────────────────────────────────\n", "GT_RAW_PATH = \"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/Ground_truth_first_20_not_normalised/1to17.md\"\n", "HYP_RAW_PATH = \"/home/skiredj.abderrahman/khalil/Benchmarking_OCR/tests_deepseek2_no_split/output_markdown_no_pageN/images_de_tests.md\"\n", "MODEL_NAME = \"Deepseek2_without_split\"\n", "\n", "# ── Normalization ─────────────────────────────────────────────────────────────\n", "def normalize_arabic_ocr(text: str, remove_markdown: bool = False) -> str:\n", " if not isinstance(text, str) or not text.strip():\n", " return \"\"\n", " if remove_markdown:\n", " text = re.sub(r'#{1,6}\\s*', '', text)\n", " text = re.sub(r'^\\s*[-*+]\\s+', '', text, flags=re.MULTILINE)\n", " text = re.sub(r'\\*{1,3}(.*?)\\*{1,3}', r'\\1', text, flags=re.DOTALL)\n", " text = re.sub(r'`{1,3}.*?`{1,3}', ' ', text, flags=re.DOTALL)\n", " text = re.sub(r'\\[([^\\]]*)\\]\\([^\\)]*\\)', r'\\1', text)\n", " text = re.sub(r'!\\[[^\\]]*\\]\\([^\\)]*\\)', ' ', text)\n", " text = re.sub(r'\\|', ' ', text)\n", " text = unicodedata.normalize('NFKC', text)\n", " text = re.sub(r'[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\uFEFF\\u00AD]', '', text)\n", " text = re.sub(\n", " r'[\\u064B-\\u065F\\u0610-\\u061A\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED]',\n", " '', text\n", " )\n", " text = text.replace('\\u0640', '')\n", " text = re.sub(r'[\\u0622\\u0623\\u0625\\u0671\\u0672\\u0673\\u0675]', '\\u0627', text)\n", " text = text.replace('\\u0649', '\\u064A')\n", " text = text.replace('\\u0629', '\\u0647')\n", " text = text.replace('\\u0624', '\\u0648')\n", " text = text.replace('\\u0626', '\\u064A')\n", " text = re.sub(r'[\\u060C\\u061B\\u061F\\u066A-\\u066D\\u0600-\\u0605]', ' ', text)\n", " text = re.sub(r'[.,:;()\\[\\]{}\\-«»\"\\'!?/\\\\~@#$%^&*_+=<>|]', ' ', text)\n", " text = text.lower()\n", " text = re.sub(r'[^\\u0600-\\u06FF\\u0750-\\u077Fa-z0-9\\s]', ' ', text)\n", " text = text.translate(str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789'))\n", " text = text.lower()\n", " return re.sub(r'\\s+', ' ', text).strip()\n", "\n", "# ── Page splitter ─────────────────────────────────────────────────────────────\n", "def split_pages_raw(content: str) -> dict:\n", " pattern = re.compile(r'^===\\s*(.+?)\\s*===$', re.MULTILINE)\n", " matches = list(pattern.finditer(content))\n", " if not matches:\n", " print(\"⚠️ No page separators found\")\n", " return {\"page_01\": content.strip()}\n", " pages = {}\n", " for idx, m in enumerate(matches):\n", " page_id = m.group(1).strip()\n", " start = m.end()\n", " end = matches[idx + 1].start() if idx + 1 < len(matches) else len(content)\n", " text = content[start:end].strip()\n", " if text:\n", " pages[page_id] = text\n", " return pages\n", "\n", "# ── TEDS helpers ──────────────────────────────────────────────────────────────\n", "\n", "def _is_separator_row(row: str) -> bool:\n", " \"\"\"True if row is a markdown separator like |---|---|\"\"\"\n", " cells = [c.strip() for c in row.strip().strip('|').split('|')]\n", " return bool(cells) and all(re.match(r'^[-:\\s]+$', c) for c in cells if c)\n", "\n", "\n", "def _expand_inline_table(line: str) -> Optional[str]:\n", " \"\"\"\n", " Detect and expand a single-line collapsed markdown table.\n", "\n", " The HYP model outputs tables like:\n", " header1 | header2 | ... | --- | --- | ... | cell1 | cell2 | ...\n", " all on ONE line. We detect the separator fragment '| --- |' or '|---|'\n", " and reconstruct a proper multi-line table.\n", "\n", " Returns a multi-line markdown table string, or None if not detected.\n", " \"\"\"\n", " # Must contain a separator pattern inside the line\n", " if not re.search(r'\\|\\s*-{2,}[\\s:]*\\|', line):\n", " return None\n", "\n", " # Tokenise: split on '|' but keep content\n", " raw_cells = [c.strip() for c in line.split('|')]\n", " # Remove leading/trailing empty strings from splitting\n", " while raw_cells and raw_cells[0] == '':\n", " raw_cells.pop(0)\n", " while raw_cells and raw_cells[-1] == '':\n", " raw_cells.pop()\n", "\n", " if not raw_cells:\n", " return None\n", "\n", " # Find all separator positions (cells that look like '---', ':---:', etc.)\n", " sep_positions = [i for i, c in enumerate(raw_cells) if re.match(r'^[-:\\s]+$', c) and len(c) >= 2]\n", "\n", " if not sep_positions:\n", " return None\n", "\n", " # We expect separators to appear as a consecutive block.\n", " # Strategy: find the first run of separator cells → that marks the header/sep boundary.\n", " # Rows are inferred by counting columns = number of separators.\n", " n_cols = len(sep_positions)\n", "\n", " # The separator cells should be consecutive; find where they start\n", " sep_start = sep_positions[0]\n", "\n", " # Everything before the separator block = header cells\n", " header_cells = raw_cells[:sep_start]\n", "\n", " # If header has more cells than n_cols, it might contain preamble text.\n", " # Trim header to last n_cols cells.\n", " if len(header_cells) > n_cols:\n", " header_cells = header_cells[len(header_cells) - n_cols:]\n", " elif len(header_cells) < n_cols:\n", " # Pad header with empty cells\n", " header_cells = [''] * (n_cols - len(header_cells)) + header_cells\n", "\n", " # Everything after the separator block = data cells\n", " data_cells = raw_cells[sep_start + n_cols:]\n", "\n", " # Build rows from data_cells in chunks of n_cols\n", " rows = []\n", " for i in range(0, len(data_cells), n_cols):\n", " chunk = data_cells[i:i + n_cols]\n", " # Pad incomplete last row\n", " while len(chunk) < n_cols:\n", " chunk.append('')\n", " rows.append(chunk)\n", "\n", " if not rows:\n", " return None\n", "\n", " # Reconstruct multi-line markdown table\n", " sep_row = '| ' + ' | '.join(['---'] * n_cols) + ' |'\n", " header_row = '| ' + ' | '.join(header_cells) + ' |'\n", " data_rows = ['| ' + ' | '.join(r) + ' |' for r in rows]\n", "\n", " return '\\n'.join([header_row, sep_row] + data_rows)\n", "\n", "\n", "def extract_md_tables(text: str) -> list:\n", " \"\"\"\n", " Extract markdown tables from text.\n", " Handles both:\n", " - Normal multi-line markdown tables (one row per line)\n", " - Collapsed single-line tables (entire table on one line, as produced by some OCR models)\n", " \"\"\"\n", " tables = []\n", " lines = text.split('\\n')\n", " cur = []\n", "\n", " for line in lines:\n", " stripped = line.strip()\n", "\n", " # ── Try to detect a collapsed single-line table ───────────────────\n", " if re.search(r'\\|\\s*-{2,}[\\s:]*\\|', stripped):\n", " # Flush any in-progress multi-line table first\n", " if len(cur) >= 2:\n", " tables.append('\\n'.join(cur))\n", " cur = []\n", " expanded = _expand_inline_table(stripped)\n", " if expanded:\n", " tables.append(expanded)\n", " continue # Don't add this raw line to cur\n", "\n", " # ── Normal multi-line table accumulation ──────────────────────────\n", " if stripped.startswith('|') or stripped.count('|') >= 2:\n", " cur.append(stripped)\n", " else:\n", " if len(cur) >= 2:\n", " tables.append('\\n'.join(cur))\n", " cur = []\n", "\n", " if len(cur) >= 2:\n", " tables.append('\\n'.join(cur))\n", "\n", " return tables\n", "\n", "\n", "def md_table_to_html(md: str) -> str:\n", " lines = [l.strip() for l in md.strip().split('\\n') if l.strip()]\n", " lines = [l for l in lines if not _is_separator_row(l)]\n", " if not lines:\n", " return '
'\n", " rows = []\n", " for i, line in enumerate(lines):\n", " cells = [re.sub(r'\\s+', ' ', c.strip()) for c in line.strip('|').split('|')]\n", " tag = 'th' if i == 0 else 'td'\n", " rows.append('' + ''.join(f'<{tag}>{_html.escape(c)}' for c in cells) + '')\n", " return '' + ''.join(rows) + '
'\n", "\n", "\n", "class AptedConfig(Config):\n", " def rename(self, node1, node2):\n", " t1 = re.sub(r'\\s+', ' ', (node1.text or '').strip())\n", " t2 = re.sub(r'\\s+', ' ', (node2.text or '').strip())\n", " return 0 if (node1.tag == node2.tag and t1 == t2) else 1\n", " def children(self, node):\n", " return list(node)\n", "\n", "def teds_score(ref_html: str, hyp_html: str) -> Optional[float]:\n", " try:\n", " rt = etree.fromstring(f'{ref_html}')\n", " ht = etree.fromstring(f'{hyp_html}')\n", " dist = APTED(rt, ht, AptedConfig()).compute_edit_distance()\n", " n = max(len(list(rt.iter())), len(list(ht.iter())))\n", " return round((1.0 - dist / n) * 100, 2) if n > 0 else 0.0\n", " except Exception as e:\n", " print(f'TEDS error: {e}')\n", " return None\n", "\n", "# ── Per-page evaluation ───────────────────────────────────────────────────────\n", "def evaluate_page(ref_raw: str, hyp_raw: str) -> dict:\n", " ref = normalize_arabic_ocr(ref_raw, remove_markdown=True)\n", " hyp = normalize_arabic_ocr(hyp_raw, remove_markdown=True)\n", "\n", " if not ref or not hyp:\n", " return {'status': 'empty_after_norm'}\n", "\n", " rouge_sc = rs.RougeScorer(['rouge1', 'rougeL'], use_stemmer=False)\n", " pg = {'status': 'ok', 'ref_chars': len(ref_raw), 'hyp_chars': len(hyp_raw)}\n", "\n", " # NED\n", " try:\n", " ed = editdistance.eval(ref, hyp)\n", " pg['NED'] = round(ed / max(len(ref), len(hyp), 1) * 100, 2)\n", " except: pg['NED'] = None\n", "\n", " # CER / WER — jiwer\n", " try:\n", " tf = jiwer.Compose([jiwer.Strip(), jiwer.ReduceToListOfListOfWords()])\n", " pg['CER_jiwer'] = round(jiwer.cer(ref, hyp) * 100, 2)\n", " pg['WER_jiwer'] = round(jiwer.wer(ref, hyp, reference_transform=tf, hypothesis_transform=tf) * 100, 2)\n", " except: pg['CER_jiwer'] = pg['WER_jiwer'] = None\n", "\n", " # CER / WER — dinglehopper\n", " try:\n", " from dinglehopper.character_error_rate import character_error_rate as dce\n", " from dinglehopper.word_error_rate import word_error_rate as dwe\n", " pg['CER_dh'] = round(float(dce(ref, hyp)) * 100, 2)\n", " pg['WER_dh'] = round(float(dwe(ref, hyp)) * 100, 2)\n", " except: pg['CER_dh'] = pg['WER_dh'] = None\n", "\n", " # BLEU\n", " try:\n", " pg['BLEU'] = round(sacrebleu.sentence_bleu(hyp, [ref]).score, 2)\n", " except: pg['BLEU'] = None\n", "\n", " # ROUGE\n", " try:\n", " r = rouge_sc.score(ref, hyp)\n", " pg['ROUGE_1'] = round(r['rouge1'].fmeasure * 100, 2)\n", " pg['ROUGE_L'] = round(r['rougeL'].fmeasure * 100, 2)\n", " except: pg['ROUGE_1'] = pg['ROUGE_L'] = None\n", "\n", " # TEDS — on raw text (uses the fixed extractor)\n", " ref_tables = extract_md_tables(ref_raw)\n", " hyp_tables = extract_md_tables(hyp_raw)\n", " pg['n_tables_ref'] = len(ref_tables)\n", " pg['n_tables_hyp'] = len(hyp_tables)\n", " if ref_tables:\n", " teds_list = []\n", " for i, rt in enumerate(ref_tables):\n", " score = teds_score(md_table_to_html(rt), md_table_to_html(hyp_tables[i])) \\\n", " if i < len(hyp_tables) else 0.0\n", " teds_list.append(score if score is not None else 0.0)\n", " pg['TEDS'] = round(sum(teds_list) / len(teds_list), 2)\n", " else:\n", " pg['TEDS'] = None\n", "\n", " return pg\n", "\n", "# ── Main ──────────────────────────────────────────────────────────────────────\n", "gt_raw_content = open(GT_RAW_PATH, encoding='utf-8').read().strip()\n", "hyp_raw_content = open(HYP_RAW_PATH, encoding='utf-8').read().strip()\n", "\n", "gt_pages = split_pages_raw(gt_raw_content)\n", "hyp_pages = split_pages_raw(hyp_raw_content)\n", "\n", "print(f'GT pages : {len(gt_pages)} → {list(gt_pages.keys())}')\n", "print(f'HYP pages : {len(hyp_pages)} → {list(hyp_pages.keys())}')\n", "\n", "common = sorted(set(gt_pages) & set(hyp_pages))\n", "only_gt = set(gt_pages) - set(hyp_pages)\n", "only_hyp = set(hyp_pages) - set(gt_pages)\n", "if only_gt: print(f'⚠️ Only in GT : {sorted(only_gt)}')\n", "if only_hyp: print(f'⚠️ Only in HYP : {sorted(only_hyp)}')\n", "print(f'\\n✅ Common pages to evaluate : {len(common)}')\n", "\n", "per_page = {}\n", "for page_id in common:\n", " per_page[page_id] = evaluate_page(gt_pages[page_id], hyp_pages[page_id])\n", "\n", "# ── Global averages ───────────────────────────────────────────────────────────\n", "def avg(k):\n", " vals = [v[k] for v in per_page.values()\n", " if isinstance(v, dict) and v.get('status') == 'ok' and v.get(k) is not None]\n", " return round(sum(vals) / len(vals), 2) if vals else None\n", "\n", "overall = {k: avg(k) for k in ['NED','CER_jiwer','WER_jiwer','CER_dh','WER_dh','BLEU','ROUGE_1','ROUGE_L','TEDS']}\n", "\n", "# ── Print results ─────────────────────────────────────────────────────────────\n", "SEP = '=' * 60\n", "print(f'\\n{SEP}')\n", "print(f' {MODEL_NAME} — {len(common)} pages')\n", "print(SEP)\n", "print(f\" NED : {overall['NED']}% [↓ principal]\")\n", "print(f\" CER_jiwer : {overall['CER_jiwer']}% WER_jiwer : {overall['WER_jiwer']}%\")\n", "print(f\" CER_dh : {overall['CER_dh']}% WER_dh : {overall['WER_dh']}%\")\n", "print(f\" BLEU : {overall['BLEU']} ROUGE-1 : {overall['ROUGE_1']}% ROUGE-L : {overall['ROUGE_L']}%\")\n", "print(f\" TEDS : {overall['TEDS']}% [None = no tables in GT page]\")\n", "print(SEP)\n", "\n", "print(f\"\\n{'─'*60}\")\n", "print(f' Per-page breakdown')\n", "print(f\"{'─'*60}\")\n", "for page_id, pg in per_page.items():\n", " if pg.get('status') == 'ok':\n", " print(f'\\n {page_id}')\n", " print(f\" NED:{pg['NED']}% CER:{pg['CER_jiwer']}% WER:{pg['WER_jiwer']}% BLEU:{pg['BLEU']} ROUGE-1:{pg['ROUGE_1']}% TEDS:{pg['TEDS']}%\")\n", " if pg['n_tables_ref'] or pg['n_tables_hyp']:\n", " print(f\" tables → GT:{pg['n_tables_ref']} HYP:{pg['n_tables_hyp']}\")\n", " else:\n", " print(f\"\\n {page_id} ⚠️ {pg.get('status')}\")" ] } ], "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 }