Picarones / rapport_demo.html
Claude
chore: supprime toutes les mentions "département numérique"
1af6b22 unverified
Raw
History Blame
178 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Picarones — Corpus de test — Chroniques médiévales BnF</title>
<!-- Chart.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"
integrity="sha512-CQBWl4fJHWbryGE+Pc3UJWW1h3Q8IkkvNnPTozals+S49OTEQPoQj/m1LZRM28Wr/7bJCMlpYS3/Zp4hHuWQ=="
crossorigin="anonymous"></script>
<!-- diff2html -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/diff2html/3.4.47/diff2html.min.css"
crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/diff2html/3.4.47/diff2html.min.js"
crossorigin="anonymous"></script>
<style>
/* ── Reset & base ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f1f5f9;
--surface: #ffffff;
--border: #e2e8f0;
--primary: #1e40af;
--primary-lt: #dbeafe;
--text: #1e293b;
--text-muted: #64748b;
--ins: #16a34a;
--ins-bg: #dcfce7;
--del: #dc2626;
--del-bg: #fee2e2;
--rep: #c2410c;
--rep-bg: #ffedd5;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);
--nav-h: 56px;
}
html { font-size: 14px; scroll-behavior: smooth; }
body {
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* ── Navigation ───────────────────────────────────────────────────── */
nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
height: var(--nav-h);
background: var(--primary);
display: flex; align-items: center;
padding: 0 1.5rem;
gap: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,.25);
}
nav .brand {
color: #fff; font-weight: 700; font-size: 1.1rem;
letter-spacing: -.3px; white-space: nowrap;
display: flex; align-items: center; gap: .4rem;
}
nav .brand span { opacity: .7; font-weight: 400; font-size: .85rem; }
nav .tabs {
display: flex; gap: .25rem; flex: 1;
}
.tab-btn {
background: transparent; border: none; cursor: pointer;
color: rgba(255,255,255,.7);
padding: .4rem .9rem; border-radius: 6px;
font-size: .9rem; font-weight: 500;
transition: background .15s, color .15s;
}
.tab-btn:hover { background: rgba(255,255,255,.12); color: #fff; }
.tab-btn.active { background: rgba(255,255,255,.18); color: #fff; }
nav .meta {
color: rgba(255,255,255,.6); font-size: .78rem;
white-space: nowrap; margin-left: auto;
}
/* ── Layout ───────────────────────────────────────────────────────── */
main {
margin-top: var(--nav-h);
padding: 1.5rem;
max-width: 1400px;
margin-left: auto; margin-right: auto;
}
.view { display: none; }
.view.active { display: block; }
.card {
background: var(--surface);
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: var(--shadow);
padding: 1.25rem;
margin-bottom: 1.25rem;
}
h2 {
font-size: 1rem; font-weight: 700;
color: var(--text); margin-bottom: .75rem;
border-bottom: 2px solid var(--primary-lt);
padding-bottom: .4rem;
}
h3 { font-size: .9rem; font-weight: 600; margin-bottom: .5rem; }
/* ── Ranking table ────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; }
table {
width: 100%; border-collapse: collapse;
font-size: .88rem;
}
thead tr { background: var(--bg); }
th {
text-align: left; padding: .6rem .75rem;
border-bottom: 2px solid var(--border);
cursor: pointer; white-space: nowrap;
color: var(--text-muted); font-weight: 600; font-size: .8rem;
text-transform: uppercase; letter-spacing: .04em;
user-select: none;
}
th.sortable:hover { color: var(--primary); }
th .sort-icon { opacity: .4; margin-left: .25rem; font-style: normal; }
th.sorted .sort-icon { opacity: 1; color: var(--primary); }
td {
padding: .55rem .75rem;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #f8fafc; }
.rank-badge {
display: inline-flex; align-items: center; justify-content: center;
width: 1.6rem; height: 1.6rem; border-radius: 50%;
font-weight: 700; font-size: .75rem;
background: var(--primary-lt); color: var(--primary);
}
.rank-badge.rank-1 { background: #fef3c7; color: #92400e; }
.engine-name { font-weight: 600; }
.engine-version { color: var(--text-muted); font-size: .78rem; margin-left: .3rem; }
.cer-badge {
display: inline-block;
padding: .15rem .5rem; border-radius: 4px;
font-weight: 600; font-size: .82rem;
}
.bar {
display: inline-block; height: 8px; border-radius: 4px;
vertical-align: middle; margin-right: .4rem;
}
/* ── Gallery ──────────────────────────────────────────────────────── */
.gallery-controls {
display: flex; align-items: center; gap: .75rem;
margin-bottom: 1rem; flex-wrap: wrap;
}
.gallery-controls label { font-size: .82rem; color: var(--text-muted); }
.gallery-controls input[type=range] { width: 120px; }
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.gallery-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
cursor: pointer;
transition: transform .15s, box-shadow .15s;
}
.gallery-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,.12);
border-color: var(--primary);
}
.gallery-card img, .gallery-card .img-placeholder {
width: 100%; aspect-ratio: 4/3; object-fit: cover;
display: block; background: #e8e0d4;
}
.img-placeholder {
display: flex; align-items: center; justify-content: center;
font-size: 2rem; color: #94a3b8;
}
.gallery-card-body {
padding: .6rem .75rem;
}
.gallery-card-title {
font-size: .8rem; font-weight: 600; margin-bottom: .35rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gallery-card-badges {
display: flex; gap: .3rem; flex-wrap: wrap;
}
.engine-cer-badge {
font-size: .7rem; font-weight: 700;
padding: .1rem .35rem; border-radius: 3px;
}
/* ── Document detail ──────────────────────────────────────────────── */
.doc-layout {
display: grid;
grid-template-columns: 220px 1fr;
gap: 1rem;
align-items: start;
}
@media (max-width: 768px) {
.doc-layout { grid-template-columns: 1fr; }
}
.doc-sidebar {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
max-height: calc(100vh - var(--nav-h) - 3rem);
overflow-y: auto;
position: sticky;
top: calc(var(--nav-h) + 1.5rem);
}
.doc-sidebar-header {
padding: .6rem .75rem;
font-size: .8rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: .05em;
border-bottom: 1px solid var(--border);
position: sticky; top: 0; background: var(--surface);
}
.doc-list-item {
padding: .5rem .75rem;
cursor: pointer;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
gap: .5rem;
transition: background .1s;
}
.doc-list-item:last-child { border-bottom: none; }
.doc-list-item:hover { background: var(--bg); }
.doc-list-item.active { background: var(--primary-lt); }
.doc-list-label { font-size: .82rem; font-weight: 500; }
.doc-list-cer {
font-size: .72rem; font-weight: 700;
padding: .1rem .3rem; border-radius: 3px;
flex-shrink: 0;
}
/* Image zone */
.doc-image-wrap {
position: relative; overflow: hidden;
border: 1px solid var(--border); border-radius: var(--radius);
background: #e8e0d4; cursor: zoom-in;
aspect-ratio: 4/3;
}
.doc-image-wrap img {
width: 100%; height: 100%; object-fit: contain;
transform-origin: center center;
transition: transform .2s;
user-select: none;
}
.doc-image-placeholder {
width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
flex-direction: column; gap: .5rem; color: #94a3b8;
font-size: .9rem;
}
.zoom-controls {
position: absolute; bottom: .5rem; right: .5rem;
display: flex; gap: .3rem;
}
.zoom-btn {
background: rgba(0,0,0,.5); color: #fff;
border: none; border-radius: 4px; cursor: pointer;
width: 28px; height: 28px; font-size: .9rem;
display: flex; align-items: center; justify-content: center;
transition: background .1s;
}
.zoom-btn:hover { background: rgba(0,0,0,.75); }
/* Diff panels */
.diff-panels {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: .75rem;
margin-top: .75rem;
}
.diff-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.diff-panel-header {
padding: .5rem .75rem;
background: var(--bg);
border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
}
.diff-panel-title { font-size: .83rem; font-weight: 700; }
.diff-panel-metrics {
display: flex; gap: .4rem;
font-size: .72rem;
}
.diff-panel-body {
padding: .75rem; font-size: .82rem; line-height: 1.7;
font-family: 'Georgia', serif;
max-height: 260px; overflow-y: auto;
}
/* Diff spans */
.d-eq { color: var(--text); }
.d-ins { color: var(--ins); background: var(--ins-bg); border-radius: 2px; padding: 0 1px; }
.d-del { color: var(--del); background: var(--del-bg); border-radius: 2px; padding: 0 1px; text-decoration: line-through; }
.d-rep-old { color: var(--del); background: var(--del-bg); border-radius: 2px 0 0 2px; padding: 0 1px; text-decoration: line-through; }
.d-rep-new { color: var(--rep); background: var(--rep-bg); border-radius: 0 2px 2px 0; padding: 0 1px; }
/* GT panel */
.gt-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.gt-panel-header {
padding: .5rem .75rem;
background: #f0fdf4;
border-bottom: 1px solid #bbf7d0;
font-size: .83rem; font-weight: 700; color: #15803d;
}
.gt-panel-body {
padding: .75rem; font-size: .82rem; line-height: 1.7;
font-family: 'Georgia', serif;
max-height: 260px; overflow-y: auto;
color: var(--text);
}
/* ── Analyses ─────────────────────────────────────────────────────── */
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 1rem;
}
.chart-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
}
.chart-canvas-wrap { position: relative; height: 280px; }
/* ── Pipeline badges ──────────────────────────────────────────────── */
.pipeline-tag {
display: inline-flex; align-items: center; gap: .25rem;
padding: .12rem .38rem;
border-radius: 4px; font-size: .67rem; font-weight: 700;
background: #ede9fe; color: #6d28d9;
letter-spacing: .02em; vertical-align: middle;
}
.pipeline-tag .pipe-arrow { opacity: .7; }
.over-norm-badge {
display: inline-block; padding: .12rem .38rem;
border-radius: 4px; font-size: .67rem; font-weight: 700;
background: #fef3c7; color: #b45309;
}
.over-norm-badge.high { background: #fee2e2; color: #b91c1c; }
/* Vue triple-diff (pipeline) */
.triple-diff-wrap {
display: grid; grid-template-columns: 1fr 1fr; gap: .5rem;
margin-top: .5rem;
}
.triple-diff-section { background: var(--bg); border-radius: 6px; padding: .5rem; }
.triple-diff-section h5 {
font-size: .73rem; font-weight: 700; color: var(--text-muted);
margin-bottom: .35rem; text-transform: uppercase; letter-spacing: .04em;
}
.pipeline-steps {
display: flex; align-items: center; gap: .3rem; flex-wrap: wrap;
margin-top: .25rem;
}
.step-chip {
padding: .12rem .4rem; border-radius: 4px; font-size: .68rem; font-weight: 600;
}
.step-chip.ocr { background: #e0f2fe; color: #0369a1; }
.step-chip.llm { background: #ede9fe; color: #6d28d9; }
.step-arrow { color: var(--text-muted); font-size: .8rem; }
/* ── Misc ─────────────────────────────────────────────────────────── */
.badge {
display: inline-block; padding: .15rem .45rem;
border-radius: 4px; font-size: .72rem; font-weight: 700;
}
.pill {
display: inline-block; padding: .1rem .4rem;
border-radius: 12px; font-size: .72rem;
background: var(--primary-lt); color: var(--primary);
}
.empty-state {
text-align: center; padding: 3rem 1rem;
color: var(--text-muted); font-size: .9rem;
}
.legend-dot {
display: inline-block; width: 8px; height: 8px;
border-radius: 50%; margin-right: .3rem;
}
.legend-row {
display: flex; align-items: center; gap: .4rem;
font-size: .78rem; color: var(--text-muted);
}
footer {
text-align: center; padding: 1.5rem;
color: var(--text-muted); font-size: .75rem;
border-top: 1px solid var(--border); margin-top: 2rem;
}
.stat-row {
display: flex; gap: 1.5rem; flex-wrap: wrap; margin-bottom: .75rem;
}
.stat {
background: var(--bg); border-radius: 6px; padding: .4rem .75rem;
font-size: .8rem;
}
.stat b { color: var(--primary); }
/* ── Difficulty badge ─────────────────────────────────────────── */
.diff-badge {
display: inline-flex; align-items: center; gap: .2rem;
padding: .1rem .4rem; border-radius: 4px;
font-size: .7rem; font-weight: 700;
}
/* ── Presentation mode ────────────────────────────────────────── */
.btn-present {
background: rgba(255,255,255,.15); border: 1px solid rgba(255,255,255,.3);
color: #fff; padding: .3rem .7rem; border-radius: 6px;
font-size: .8rem; font-weight: 600; cursor: pointer;
transition: background .15s;
white-space: nowrap;
}
.btn-present:hover { background: rgba(255,255,255,.28); }
.btn-present.active { background: rgba(255,255,255,.35); }
.btn-export-csv {
background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.25);
color: rgba(255,255,255,.85); padding: .3rem .7rem; border-radius: 6px;
font-size: .8rem; font-weight: 600; cursor: pointer;
transition: background .15s; white-space: nowrap;
}
.btn-export-csv:hover { background: rgba(255,255,255,.22); color:#fff; }
body.present-mode .technical { display: none !important; }
body.present-mode .chart-card { page-break-inside: avoid; }
body.present-mode nav .meta { display: none; }
/* ── Cluster cards ─────────────────────────────────────────────── */
.cluster-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: .75rem; margin-top: .75rem;
}
.cluster-card {
background: var(--bg); border: 1px solid var(--border);
border-radius: var(--radius); padding: .75rem;
}
.cluster-label { font-weight: 700; font-size: .88rem; color: var(--primary); margin-bottom: .3rem; }
.cluster-count { font-size: .75rem; color: var(--text-muted); margin-bottom: .5rem; }
.cluster-examples {
display: flex; flex-direction: column; gap: .2rem;
}
.cluster-ex {
font-family: monospace; font-size: .78rem;
background: var(--surface); border-radius: 3px; padding: .15rem .35rem;
display: flex; align-items: center; gap: .35rem; color: var(--text-muted);
}
.cluster-ex .ex-old { color: var(--del); background: var(--del-bg); border-radius: 2px; padding: 0 3px; }
.cluster-ex .ex-new { color: var(--rep); background: var(--rep-bg); border-radius: 2px; padding: 0 3px; }
/* ── Statistical tests table ─────────────────────────────────────*/
.stat-sig { color: #dc2626; font-weight: 700; }
.stat-ns { color: #64748b; }
/* ── Venn diagram ────────────────────────────────────────────────*/
.venn-wrap { display: flex; justify-content: center; padding: 1rem; }
/* ── Correlation matrix ──────────────────────────────────────────*/
.corr-table { border-collapse: collapse; font-size: .8rem; margin: .5rem auto; }
.corr-table th, .corr-table td {
padding: .35rem .5rem; text-align: center; border: 1px solid var(--border);
min-width: 60px;
}
.corr-table th { background: var(--bg); font-weight: 600; font-size: .75rem; }
/* ── Sprint 10 — heatmap erreurs ─────────────────────────────────*/
.heatmap-wrap {
display: flex; gap: 3px; align-items: flex-end;
height: 60px; margin: .5rem 0;
}
.heatmap-bar {
flex: 1; border-radius: 3px 3px 0 0;
min-height: 4px;
transition: opacity .15s;
}
.heatmap-bar:hover { opacity: .75; }
.heatmap-labels {
display: flex; justify-content: space-between;
font-size: .65rem; color: var(--text-muted); margin-top: .15rem;
}
/* ── Sprint 10 — hallucination badge ─────────────────────────────*/
.hallucination-badge {
display: inline-flex; align-items: center; gap: .25rem;
padding: .15rem .45rem; border-radius: 4px;
font-size: .72rem; font-weight: 700;
background: #fce7f3; color: #9d174d;
border: 1px solid #fbcfe8;
}
.hallucination-badge.ok {
background: #f0fdf4; color: #15803d;
border-color: #bbf7d0;
}
/* ── Sprint 10 — bloc halluciné ──────────────────────────────────*/
.halluc-block {
background: #fce7f3; border: 1px solid #f9a8d4;
border-radius: 4px; padding: .35rem .6rem;
margin: .25rem 0; font-size: .78rem;
font-family: 'Georgia', serif; color: #9d174d;
}
.halluc-block-meta {
font-size: .65rem; color: #be185d; font-family: system-ui, sans-serif;
margin-bottom: .15rem; font-weight: 600;
}
/* ── Sprint 10 — percentile bars ─────────────────────────────────*/
.pct-bars { display: flex; flex-direction: column; gap: .25rem; margin: .4rem 0; }
.pct-bar-row { display: flex; align-items: center; gap: .4rem; font-size: .72rem; }
.pct-bar-label { width: 2.5rem; color: var(--text-muted); text-align: right; flex-shrink: 0; }
.pct-bar-track {
flex: 1; height: 8px; background: var(--bg);
border-radius: 4px; overflow: hidden;
}
.pct-bar-fill { height: 100%; border-radius: 4px; }
.pct-bar-val { width: 3rem; color: var(--text); font-weight: 600; }
</style>
</head>
<body>
<!-- ── Navigation ─────────────────────────────────────────────────── -->
<nav>
<div class="brand">
Picarones
<span data-i18n="nav_report">| rapport OCR</span>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="showView('ranking')" data-i18n="tab_ranking">Classement</button>
<button class="tab-btn" onclick="showView('gallery')" data-i18n="tab_gallery">Galerie</button>
<button class="tab-btn" onclick="showView('document')" data-i18n="tab_document">Document</button>
<button class="tab-btn" onclick="showView('characters')" data-i18n="tab_characters">Caractères</button>
<button class="tab-btn" onclick="showView('analyses')" data-i18n="tab_analyses">Analyses</button>
</div>
<div class="meta" id="nav-meta"></div>
<button class="btn-export-csv" onclick="exportCSV()" title="⬇ CSV">⬇ CSV</button>
<button class="btn-present" id="btn-present" onclick="togglePresentMode()" data-i18n="btn_present">⊞ Présentation</button>
</nav>
<!-- ── Main ───────────────────────────────────────────────────────── -->
<main>
<!-- ════ Vue 1 : Classement ════════════════════════════════════════ -->
<div id="view-ranking" class="view active">
<div class="card">
<h2 data-i18n="h_ranking">Classement des moteurs</h2>
<div class="stat-row" id="ranking-stats"></div>
<div class="table-wrap">
<table id="ranking-table">
<thead>
<tr>
<th data-col="rank" class="sortable sorted" data-dir="asc" data-i18n="col_rank">#<i class="sort-icon"></i></th>
<th data-col="name" class="sortable" data-i18n="col_engine">Concurrent<i class="sort-icon"></i></th>
<th data-col="cer" class="sortable" data-i18n="col_cer">CER exact<i class="sort-icon"></i></th>
<th data-col="cer_diplomatic" class="sortable" id="th-cer-diplo" data-i18n="col_cer_diplo">CER diplo.<i class="sort-icon"></i></th>
<th data-col="wer" class="sortable" data-i18n="col_wer">WER<i class="sort-icon"></i></th>
<th data-col="mer" class="sortable" data-i18n="col_mer">MER<i class="sort-icon"></i></th>
<th data-col="wil" class="sortable" data-i18n="col_wil">WIL<i class="sort-icon"></i></th>
<th data-col="ligature_score" class="sortable" id="th-ligatures" data-i18n="col_ligatures">Ligatures<i class="sort-icon"></i></th>
<th data-col="diacritic_score" class="sortable" id="th-diacritics" data-i18n="col_diacritics">Diacritiques<i class="sort-icon"></i></th>
<th data-col="gini" class="sortable" id="th-gini" data-i18n="col_gini">Gini<i class="sort-icon"></i></th>
<th data-col="anchor_score" class="sortable" id="th-anchor" data-i18n="col_anchor">Ancrage<i class="sort-icon"></i></th>
<th data-i18n="col_cer_median">CER médian</th>
<th data-i18n="col_cer_min">CER min</th>
<th data-i18n="col_cer_max">CER max</th>
<th id="th-overnorm" data-i18n="col_overnorm">Sur-norm.</th>
<th data-i18n="col_docs">Docs</th>
</tr>
</thead>
<tbody id="ranking-tbody"></tbody>
</table>
</div>
<div class="stat-row" style="margin-top:.75rem">
<div class="legend-row">
<span class="legend-dot" style="background:#16a34a"></span>CER &lt; 5 %
</div>
<div class="legend-row">
<span class="legend-dot" style="background:#ca8a04"></span>5–15 %
</div>
<div class="legend-row">
<span class="legend-dot" style="background:#ea580c"></span>15–30 %
</div>
<div class="legend-row">
<span class="legend-dot" style="background:#dc2626"></span>&gt; 30 %
</div>
</div>
</div>
</div>
<!-- ════ Vue 2 : Galerie ═══════════════════════════════════════════ -->
<div id="view-gallery" class="view">
<div class="card">
<h2 data-i18n="h_gallery">Galerie des documents</h2>
<div class="gallery-controls">
<label><span data-i18n="gallery_sort_label">Trier par :</span>
<select id="gallery-sort" onchange="renderGallery()">
<option value="doc_id" data-i18n-opt="gallery_sort_id">Identifiant</option>
<option value="mean_cer" data-i18n-opt="gallery_sort_cer">CER moyen</option>
<option value="difficulty_score" data-i18n-opt="gallery_sort_difficulty">Difficulté</option>
<option value="best_engine" data-i18n-opt="gallery_sort_best">Meilleur moteur</option>
</select>
</label>
<label><span data-i18n="gallery_filter_cer_label">Filtrer CER &gt;</span>
<input type="number" id="gallery-filter-cer" min="0" max="100" value="0" step="1"
style="width:60px" onchange="renderGallery()"> %
</label>
<label><span data-i18n="gallery_filter_engine_label">Moteur :</span>
<select id="gallery-engine-select" onchange="renderGallery()">
<option value="" data-i18n-opt="gallery_filter_all">Tous</option>
</select>
</label>
</div>
<div id="gallery-grid" class="gallery-grid"></div>
<div id="gallery-empty" class="empty-state" style="display:none" data-i18n="gallery_empty">
Aucun document ne correspond aux filtres.
</div>
</div>
</div>
<!-- ════ Vue 3 : Document ══════════════════════════════════════════ -->
<div id="view-document" class="view">
<div class="doc-layout">
<!-- Sidebar -->
<aside class="doc-sidebar">
<div class="doc-sidebar-header" data-i18n="doc_sidebar_header">Documents</div>
<div id="doc-list"></div>
</aside>
<!-- Contenu principal -->
<div>
<div class="card" id="doc-detail-header">
<div style="display:flex; align-items:baseline; justify-content:space-between; flex-wrap:wrap; gap:.5rem">
<h2 id="doc-detail-title" data-i18n="doc_title_default">Sélectionner un document</h2>
<div class="stat-row" id="doc-detail-metrics"></div>
</div>
</div>
<!-- Image zoomable -->
<div class="card">
<h3 data-i18n="h_image">Image originale</h3>
<div class="doc-image-wrap" id="doc-image-wrap"
onwheel="handleZoom(event)"
onmousedown="startDrag(event)"
onmousemove="doDrag(event)"
onmouseup="endDrag()"
onmouseleave="endDrag()">
<div class="doc-image-placeholder" id="doc-image-placeholder">
<span style="font-size:2rem">🖼</span>
<span>Sélectionnez un document</span>
</div>
<img id="doc-image" src="" alt="Image du document" style="display:none">
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoom(1.25)" title="Zoom +">+</button>
<button class="zoom-btn" onclick="zoom(0.8)" title="Zoom −"></button>
<button class="zoom-btn" onclick="resetZoom()" title="Réinitialiser"></button>
</div>
</div>
</div>
<!-- Vérité terrain -->
<div class="card">
<h3 data-i18n="h_gt">Vérité terrain (GT)</h3>
<div class="gt-panel">
<div class="gt-panel-header">✓ Ground Truth</div>
<div class="gt-panel-body" id="doc-gt-text"></div>
</div>
</div>
<!-- Diffs par moteur -->
<div class="card">
<h3 data-i18n="h_diff">Sorties OCR — diff par moteur</h3>
<div class="diff-panels" id="doc-diff-panels"></div>
</div>
<!-- Sprint 10 — Distribution CER par ligne -->
<div class="card" id="doc-line-metrics-card" style="display:none">
<h3 data-i18n="h_line_metrics">Distribution des erreurs par ligne</h3>
<div id="doc-line-metrics-content"></div>
</div>
<!-- Sprint 10 — Hallucinations détectées -->
<div class="card" id="doc-hallucination-card" style="display:none">
<h3 data-i18n="h_hallucination">Analyse des hallucinations</h3>
<div id="doc-hallucination-content"></div>
</div>
</div>
</div>
</div>
<!-- ════ Vue 4 : Analyses ══════════════════════════════════════════ -->
<div id="view-analyses" class="view">
<div class="charts-grid">
<div class="chart-card">
<h3 data-i18n="h_cer_dist">Distribution du CER par moteur</h3>
<div class="chart-canvas-wrap">
<canvas id="chart-cer-hist"></canvas>
</div>
</div>
<div class="chart-card">
<h3 data-i18n="h_radar">Profil des moteurs (radar)</h3>
<div class="chart-canvas-wrap">
<canvas id="chart-radar"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.5rem" data-i18n="radar_note">
Axe radar : CER, WER, MER, WIL — valeurs inversées (plus c'est haut, meilleur est le moteur).
</div>
</div>
<div class="chart-card">
<h3 data-i18n="h_cer_doc">CER par document (tous moteurs)</h3>
<div class="chart-canvas-wrap">
<canvas id="chart-cer-doc"></canvas>
</div>
</div>
<div class="chart-card">
<h3 data-i18n="h_duration">Temps d'exécution moyen (secondes/document)</h3>
<div class="chart-canvas-wrap">
<canvas id="chart-duration"></canvas>
</div>
</div>
<div class="chart-card">
<h3 data-i18n="h_quality_cer">Qualité image ↔ CER (scatter plot)</h3>
<div class="chart-canvas-wrap">
<canvas id="chart-quality-cer"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="quality_cer_note">
Chaque point = un document. Axe X = score qualité image [0–1]. Axe Y = CER. Corrélation négative attendue.
</div>
</div>
<div class="chart-card" style="grid-column:1/-1">
<h3 data-i18n="h_taxonomy">Taxonomie des erreurs par moteur</h3>
<div class="chart-canvas-wrap" style="max-height:300px">
<canvas id="chart-taxonomy"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="taxonomy_note">
Distribution des classes d'erreurs (classes 1–9 de la taxonomie Picarones).
</div>
</div>
<!-- Sprint 7 — Courbe de fiabilité -->
<div class="chart-card" style="grid-column:1/-1">
<h3 data-i18n="h_reliability">Courbes de fiabilité</h3>
<div class="chart-canvas-wrap" style="max-height:300px">
<canvas id="chart-reliability"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="reliability_note">
Pour les X% documents les plus faciles (triés par CER croissant), quel est le CER moyen cumulé ?
Une courbe basse = moteur performant même sur les documents faciles.
</div>
</div>
<!-- Sprint 7 — Intervalles de confiance -->
<div class="chart-card">
<h3 data-i18n="h_bootstrap">Intervalles de confiance à 95 % (bootstrap)</h3>
<div class="chart-canvas-wrap">
<canvas id="chart-bootstrap-ci"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="bootstrap_note">
IC à 95% sur le CER moyen par moteur (1000 itérations bootstrap).
</div>
</div>
<!-- Sprint 7 — Diagramme de Venn -->
<div class="chart-card">
<h3 data-i18n="h_venn">Erreurs communes / exclusives (Venn)</h3>
<div id="venn-container" style="min-height:260px;display:flex;align-items:center;justify-content:center"></div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem technical" data-i18n="venn_note">
Intersection des ensembles d'erreurs entre les 2 ou 3 premiers concurrents.
Erreurs communes = segments partagés.
</div>
</div>
<!-- Sprint 7 — Tests de Wilcoxon -->
<div class="chart-card technical">
<h3 data-i18n="h_pairwise">Tests de Wilcoxon — comparaisons par paires</h3>
<div id="wilcoxon-table-container" style="overflow-x:auto"></div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="pairwise_note">
Test signé-rangé de Wilcoxon (non-paramétrique). Seuil α = 0.05.
</div>
</div>
<!-- Sprint 7 — Clustering des erreurs -->
<div class="chart-card" style="grid-column:1/-1">
<h3 data-i18n="h_clusters">Clustering des patterns d'erreurs</h3>
<div id="error-clusters-container"></div>
</div>
<!-- Sprint 10 — Scatter Gini vs CER moyen -->
<div class="chart-card">
<h3 data-i18n="h_gini_cer">Gini vs CER moyen <span style="font-size:.72rem;font-weight:400;color:var(--text-muted)" data-i18n="gini_cer_ideal">— idéal : bas-gauche</span></h3>
<div class="chart-canvas-wrap">
<canvas id="chart-gini-cer"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="gini_cer_note">
Axe X = CER moyen, Axe Y = coefficient de Gini. Un moteur idéal a CER bas ET Gini bas (erreurs rares et uniformes).
</div>
</div>
<!-- Sprint 10 — Scatter ratio longueur vs ancrage -->
<div class="chart-card">
<h3 data-i18n="h_ratio_anchor">Ratio longueur vs ancrage <span style="font-size:.72rem;font-weight:400;color:var(--text-muted)" data-i18n="ratio_anchor_subtitle">— hallucinations VLM</span></h3>
<div class="chart-canvas-wrap">
<canvas id="chart-ratio-anchor"></canvas>
</div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="ratio_anchor_note">
Axe X = score d'ancrage trigrammes [0–1]. Axe Y = ratio longueur sortie/GT.
Zone ⚠️ : ancrage &lt; 0.5 ou ratio &gt; 1.2 → hallucinations probables.
</div>
</div>
<!-- Sprint 7 — Matrice de corrélation -->
<div class="chart-card technical" style="grid-column:1/-1">
<h3 data-i18n="h_correlation">Matrice de corrélation entre métriques</h3>
<div style="margin-bottom:.5rem">
<label style="font-size:.82rem;font-weight:600"><span data-i18n="corr_engine_label">Moteur :</span>
<select id="corr-engine-select" onchange="renderCorrelationMatrix()"
style="padding:.25rem .5rem;border-radius:6px;border:1px solid var(--border);margin-left:.25rem"></select>
</label>
</div>
<div id="corr-matrix-container" style="overflow-x:auto"></div>
<div style="font-size:.72rem;color:var(--text-muted);margin-top:.4rem" data-i18n="corr_note">
Coefficient de Pearson entre les métriques CER, WER, qualité image, ligatures, diacritiques.
Vert = corrélation positive, Rouge = corrélation négative.
</div>
</div>
</div>
</div>
<!-- ════ Vue 5 : Caractères ════════════════════════════════════════ -->
<div id="view-characters" class="view">
<div class="card">
<h2 data-i18n="h_characters">Analyse des caractères</h2>
<!-- Sélecteur de moteur -->
<div class="stat-row" style="margin-bottom:1rem">
<label for="char-engine-select" style="font-weight:600;margin-right:.5rem" data-i18n="char_engine_label">Moteur :</label>
<select id="char-engine-select" onchange="renderCharView()"
style="padding:.35rem .7rem;border-radius:6px;border:1px solid var(--border)"></select>
</div>
<!-- Scores ligatures / diacritiques -->
<div class="stat-row" id="char-scores-row" style="gap:1.5rem;margin-bottom:1.5rem"></div>
<!-- Matrice de confusion unicode -->
<h3 style="margin-bottom:.75rem">Matrice de confusion unicode
<span style="font-size:.75rem;font-weight:400;color:var(--text-muted)">
— substitutions les plus fréquentes (caractère GT → caractère OCR)
</span>
</h3>
<div id="confusion-heatmap" style="overflow-x:auto;margin-bottom:1.5rem"></div>
<!-- Détail ligatures par type -->
<h3 style="margin-bottom:.75rem">Reconnaissance des ligatures</h3>
<div id="ligature-detail" style="margin-bottom:1.5rem"></div>
<!-- Taxonomie détaillée -->
<h3 style="margin-bottom:.75rem">Distribution taxonomique des erreurs</h3>
<div id="taxonomy-detail"></div>
</div>
</div>
</main>
<footer>
<span data-i18n="footer_by">par Picarones</span> v1.0.0
<span id="footer-date"></span>
</footer>
<!-- ── Données embarquées ──────────────────────────────────────────── -->
<script>
const DATA = {"meta":{"corpus_name":"Corpus de test — Chroniques médiévales BnF","corpus_source":"/corpus/chroniques/","document_count":3,"run_date":"2026-03-08T19:17:50.622881+00:00","picarones_version":"1.0.0","metadata":{"description":"Données de démonstration générées par picarones.fixtures","script":"gothique textura","langue":"Français médiéval (XIVe-XVe siècle)","institution":"BnF — Département des manuscrits","_images_b64":{"folio_001":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADcCAIAAACOIe9xAAAC4ElEQVR4nO3bsVFDUQxFQZf4y6EIiqAIiiJ0Qu7QCRFg6Z2ZvbMFKDmhbp8f70DUbf0C4NcEDGHPgL/vX0CCgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhbCjgy8yuS8Bm4QnYLLxqwMArCBjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjDPDGZzE7BZeAI2C68aMPAKAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIezfAn4zs79tM2BgnoAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjAP/WanTMBm4W0GDMwTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQ5qHf7JQJ2Cy8zYCBeQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMI89JudMgGbhbcZMDBPwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmId+s1MmYLPwNgMG5gkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAjz0G92ygRsFt5mwMA8AUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYR76zU6ZgM3C2wwYmCdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCHMQ7/ZKROwWXibAQPzBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhHnoNztlAjYLbzNgYJ6AIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwD/1mp0zAZuFtBgzMEzCECRjCfggYyBEwhAkYwh59L5rsdXrQDgAAAABJRU5ErkJggg==","folio_002":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADcCAIAAACOIe9xAAAC4ElEQVR4nO3bsVFDUQxFQZf4y6EIiqAIiiJ0Qu7QCRFg6Z2ZvbMFKDmhbp8f70DUbf0C4NcEDGHPgL/vX0CCgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhbCjgy8yuS8Bm4QnYLLxqwMArCBjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjDPDGZzE7BZeAI2C68aMPAKAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIezfAn4zs79tM2BgnoAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjAP/WanTMBm4W0GDMwTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQ5qHf7JQJ2Cy8zYCBeQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMI89JudMgGbhbcZMDBPwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmId+s1MmYLPwNgMG5gkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAjz0G92ygRsFt5mwMA8AUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYR76zU6ZgM3C2wwYmCdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCHMQ7/ZKROwWXibAQPzBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhHnoNztlAjYLbzNgYJ6AIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwD/1mp0zAZuFtBgzMEzCECRjCfggYyBEwhAkYwh59L5rsdXrQDgAAAABJRU5ErkJggg==","folio_003":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADcCAIAAACOIe9xAAAC4ElEQVR4nO3bsVFDUQxFQZf4y6EIiqAIiiJ0Qu7QCRFg6Z2ZvbMFKDmhbp8f70DUbf0C4NcEDGHPgL/vX0CCgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhbCjgy8yuS8Bm4QnYLLxqwMArCBjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjDPDGZzE7BZeAI2C68aMPAKAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIezfAn4zs79tM2BgnoAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjAP/WanTMBm4W0GDMwTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQ5qHf7JQJ2Cy8zYCBeQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMI89JudMgGbhbcZMDBPwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmId+s1MmYLPwNgMG5gkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAjz0G92ygRsFt5mwMA8AUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYR76zU6ZgM3C2wwYmCdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCHMQ7/ZKROwWXibAQPzBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhHnoNztlAjYLbzNgYJ6AIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwD/1mp0zAZuFtBgzMEzCECRjCfggYyBEwhAkYwh59L5rsdXrQDgAAAABJRU5ErkJggg=="}}},"ranking":[{"engine":"tesseract → gpt-4o","mean_cer":0.038091,"mean_wer":0.038095,"documents":3,"failed":0},{"engine":"gpt-4o-vision (zero-shot)","mean_cer":0.038091,"mean_wer":0.038095,"documents":3,"failed":0},{"engine":"tesseract","mean_cer":0.044933,"mean_wer":0.08254,"documents":3,"failed":0},{"engine":"ancien_moteur","mean_cer":0.179834,"mean_wer":0.288889,"documents":3,"failed":0},{"engine":"pero_ocr","mean_cer":0.0,"mean_wer":0.0,"documents":3,"failed":0}],"engines":[{"name":"pero_ocr","version":"0.7.2","cer":0.0,"wer":0.0,"mer":0.0,"wil":0.0,"cer_median":0.0,"cer_min":0.0,"cer_max":0.0,"doc_count":3,"failed":0,"cer_diplomatic":0.0,"cer_diplomatic_profile":"medieval_french","cer_values":[0.0,0.0,0.0],"cer_diplomatic_values":[0.0,0.0,0.0],"is_pipeline":false,"pipeline_info":{},"ligature_score":1.0,"diacritic_score":1.0,"aggregated_confusion":{"matrix":{},"total_substitutions":0,"total_insertions":0,"total_deletions":0},"aggregated_taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":0,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.0}},"aggregated_structure":{"mean_line_fusion_rate":0.0,"mean_line_fragmentation_rate":0.0,"mean_reading_order_score":1.0,"mean_paragraph_conservation":1.0,"mean_line_accuracy":1.0,"document_count":3},"aggregated_image_quality":{"mean_quality_score":0.732,"mean_sharpness":0.614,"mean_noise_level":0.2979,"quality_distribution":{"good":2,"medium":1,"poor":0},"document_count":3,"scores":[0.5875,0.7747,0.8339]},"gini":0.0,"cer_p90":0.0,"cer_p99":0.0,"catastrophic_rate_30":0.0,"aggregated_line_metrics":{"gini_mean":0.0,"gini_stdev":0.0,"mean_cer_mean":0.0,"percentiles":{"p50":0.0,"p75":0.0,"p90":0.0,"p95":0.0,"p99":0.0},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"document_count":3},"anchor_score":1.0,"length_ratio":1.0,"hallucinating_doc_rate":0.0,"aggregated_hallucination":{"anchor_score_mean":1.0,"anchor_score_min":1.0,"length_ratio_mean":1.0,"net_insertion_rate_mean":0.0,"hallucinating_doc_count":0,"hallucinating_doc_rate":0.0,"document_count":3},"is_vlm":false},{"name":"tesseract","version":"5.3.3","cer":0.0449,"wer":0.0825,"mer":0.0825,"wil":0.139,"cer_median":0.01,"cer_min":0.009,"cer_max":0.1158,"doc_count":3,"failed":0,"cer_diplomatic":0.0513,"cer_diplomatic_profile":"medieval_french","cer_values":[0.1158,0.009,0.01],"cer_diplomatic_values":[0.125,0.009,0.0198],"is_pipeline":false,"pipeline_info":{},"ligature_score":1.0,"diacritic_score":1.0,"aggregated_confusion":{"matrix":{"c":{"∅":1},"r":{"∅":1},"o":{"∅":1},"n":{"∅":1},"i":{"∅":1},"q":{"∅":1},"u":{"∅":1},"e":{"∅":1},"s":{"∅":1},"&":{"8":2},"ſ":{"f":1}},"total_substitutions":3,"total_insertions":0,"total_deletions":9},"aggregated_taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":2,"segmentation_error":0,"oov_character":0,"lacuna":1},"total_errors":4,"class_distribution":{"visual_confusion":0.25,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.5,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.25}},"aggregated_structure":{"mean_line_fusion_rate":0.0,"mean_line_fragmentation_rate":0.0,"mean_reading_order_score":0.9274,"mean_paragraph_conservation":1.0,"mean_line_accuracy":1.0,"document_count":3},"aggregated_image_quality":{"mean_quality_score":0.7363,"mean_sharpness":0.7263,"mean_noise_level":0.2437,"quality_distribution":{"good":2,"medium":1,"poor":0},"document_count":3,"scores":[0.5284,0.8648,0.8158]},"gini":0.692,"cer_p90":0.3025,"cer_p99":0.3506,"catastrophic_rate_30":0.1667,"aggregated_line_metrics":{"gini_mean":0.692029,"gini_stdev":0.100409,"mean_cer_mean":0.133429,"percentiles":{"p50":0.088889,"p75":0.222318,"p90":0.30249,"p95":0.329214,"p99":0.350594},"catastrophic_rate":{"0.3":0.166667,"0.5":0.166667,"1.0":0.0},"heatmap":[0.0,0.0,0.0,0.0,0.011494,0.0,0.0,0.188889,0.0,0.333333],"document_count":3},"anchor_score":0.785,"length_ratio":0.9649,"hallucinating_doc_rate":0.0,"aggregated_hallucination":{"anchor_score_mean":0.784975,"anchor_score_min":0.666667,"length_ratio_mean":0.964912,"net_insertion_rate_mean":0.061905,"hallucinating_doc_count":0,"hallucinating_doc_rate":0.0,"document_count":3},"is_vlm":false},{"name":"ancien_moteur","version":"2.1.0","cer":0.1798,"wer":0.2889,"mer":0.2889,"wil":0.3963,"cer_median":0.09,"cer_min":0.0811,"cer_max":0.3684,"doc_count":3,"failed":0,"cer_diplomatic":0.1783,"cer_diplomatic_profile":"medieval_french","cer_values":[0.3684,0.0811,0.09],"cer_diplomatic_values":[0.3646,0.0811,0.0891],"is_pipeline":false,"pipeline_info":{},"ligature_score":1.0,"diacritic_score":1.0,"aggregated_confusion":{"matrix":{"p":{"∅":1},"r":{"∅":5,"z":1},"o":{"∅":3},"l":{"∅":1},"g":{"∅":1},"u":{"∅":1},"e":{"∅":5},"m":{"∅":2},"a":{"∅":3,"f":1,"w":1},"i":{"∅":2},"ſ":{"∅":3},"t":{"∅":3},"F":{"∅":2},"s":{"t":1},"n":{"∅":2},"c":{"∅":1},"E":{"∅":1},"x":{"f":1},"b":{"y":1},"J":{"z":1},"y":{"w":1},"I":{"∅":1},"d":{"∅":1}},"total_substitutions":8,"total_insertions":0,"total_deletions":38},"aggregated_taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":5,"segmentation_error":2,"oov_character":0,"lacuna":5},"total_errors":13,"class_distribution":{"visual_confusion":0.0769,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.3846,"segmentation_error":0.1538,"oov_character":0.0,"lacuna":0.3846}},"aggregated_structure":{"mean_line_fusion_rate":0.0,"mean_line_fragmentation_rate":0.0,"mean_reading_order_score":0.7697,"mean_paragraph_conservation":1.0,"mean_line_accuracy":1.0,"document_count":3},"aggregated_image_quality":{"mean_quality_score":0.4803,"mean_sharpness":0.4196,"mean_noise_level":0.4834,"quality_distribution":{"good":1,"medium":0,"poor":2},"document_count":3,"scores":[0.2888,0.388,0.7641]},"gini":0.132,"cer_p90":0.6656,"cer_p99":0.6866,"catastrophic_rate_30":0.6667,"aggregated_line_metrics":{"gini_mean":0.132039,"gini_stdev":0.026463,"mean_cer_mean":0.545813,"percentiles":{"p50":0.595594,"p75":0.630556,"p90":0.665556,"p95":0.677222,"p99":0.686556},"catastrophic_rate":{"0.3":0.666667,"0.5":0.5,"1.0":0.0},"heatmap":[0.0,0.0,0.344444,0.0,0.580076,0.0,0.0,0.611111,0.0,0.647619],"document_count":3},"anchor_score":0.33,"length_ratio":0.845,"hallucinating_doc_rate":0.6667,"aggregated_hallucination":{"anchor_score_mean":0.329966,"anchor_score_min":0.222222,"length_ratio_mean":0.845026,"net_insertion_rate_mean":0.155944,"hallucinating_doc_count":2,"hallucinating_doc_rate":0.666667,"document_count":3},"is_vlm":false},{"name":"tesseract → gpt-4o","version":"ocr=5.3.3; llm=gpt-4o","cer":0.0381,"wer":0.0381,"mer":0.0381,"wil":0.0532,"cer_median":0.009,"cer_min":0.0,"cer_max":0.1053,"doc_count":3,"failed":0,"cer_diplomatic":0.0377,"cer_diplomatic_profile":"medieval_french","cer_values":[0.1053,0.009,0.0],"cer_diplomatic_values":[0.1042,0.009,0.0],"is_pipeline":true,"pipeline_info":{"pipeline_mode":"text_and_image","prompt_file":"correction_medieval_french.txt","llm_model":"gpt-4o","llm_provider":"openai","pipeline_steps":[{"type":"ocr","engine":"tesseract","version":"5.3.3"},{"type":"llm","model":"gpt-4o","provider":"openai","mode":"text_and_image","prompt_file":"correction_medieval_french.txt"}],"over_normalization":{"score":0.0,"total_correct_ocr_words":44,"over_normalized_count":0,"document_count":3}},"ligature_score":1.0,"diacritic_score":1.0,"aggregated_confusion":{"matrix":{"c":{"∅":1},"r":{"∅":1},"o":{"∅":1},"n":{"∅":1},"i":{"∅":1},"q":{"∅":1},"u":{"∅":1},"e":{"∅":1},"s":{"∅":1},"ſ":{"f":1}},"total_substitutions":1,"total_insertions":0,"total_deletions":9},"aggregated_taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":1},"total_errors":2,"class_distribution":{"visual_confusion":0.5,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.5}},"aggregated_structure":{"mean_line_fusion_rate":0.0,"mean_line_fragmentation_rate":0.0,"mean_reading_order_score":0.9726,"mean_paragraph_conservation":1.0,"mean_line_accuracy":1.0,"document_count":3},"aggregated_image_quality":{"mean_quality_score":0.6755,"mean_sharpness":0.7034,"mean_noise_level":0.2303,"quality_distribution":{"good":1,"medium":2,"poor":0},"document_count":3,"scores":[0.6047,0.6787,0.7431]},"gini":0.4444,"cer_p90":0.2914,"cer_p99":0.3395,"catastrophic_rate_30":0.1667,"aggregated_line_metrics":{"gini_mean":0.444444,"gini_stdev":0.393818,"mean_cer_mean":0.127874,"percentiles":{"p50":0.083333,"p75":0.211207,"p90":0.291379,"p95":0.318103,"p99":0.339483},"catastrophic_rate":{"0.3":0.166667,"0.5":0.083333,"1.0":0.0},"heatmap":[0.0,0.0,0.0,0.0,0.011494,0.0,0.0,0.166667,0.0,0.333333],"document_count":3},"anchor_score":0.8918,"length_ratio":0.9649,"hallucinating_doc_rate":0.0,"aggregated_hallucination":{"anchor_score_mean":0.891813,"anchor_score_min":0.833333,"length_ratio_mean":0.964912,"net_insertion_rate_mean":0.015873,"hallucinating_doc_count":0,"hallucinating_doc_rate":0.0,"document_count":3},"is_vlm":false},{"name":"gpt-4o-vision (zero-shot)","version":"gpt-4o-2024-11-20","cer":0.0381,"wer":0.0381,"mer":0.0381,"wil":0.0532,"cer_median":0.009,"cer_min":0.0,"cer_max":0.1053,"doc_count":3,"failed":0,"cer_diplomatic":0.0377,"cer_diplomatic_profile":"medieval_french","cer_values":[0.1053,0.009,0.0],"cer_diplomatic_values":[0.1042,0.009,0.0],"is_pipeline":true,"pipeline_info":{"pipeline_mode":"zero_shot","prompt_file":"zero_shot_medieval_vlm.txt","llm_model":"gpt-4o-2024-11-20","llm_provider":"openai","pipeline_steps":[{"type":"llm","model":"gpt-4o-2024-11-20","provider":"openai","mode":"zero_shot","prompt_file":"zero_shot_medieval_vlm.txt"}],"is_vlm":true,"over_normalization":{"score":0.0,"total_correct_ocr_words":44,"over_normalized_count":0,"document_count":3}},"ligature_score":1.0,"diacritic_score":1.0,"aggregated_confusion":{"matrix":{"c":{"∅":1},"r":{"∅":1},"o":{"∅":1},"n":{"∅":1},"i":{"∅":1},"q":{"∅":1},"u":{"∅":1},"e":{"∅":1},"s":{"∅":1},"ſ":{"f":1}},"total_substitutions":1,"total_insertions":0,"total_deletions":9},"aggregated_taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":1},"total_errors":2,"class_distribution":{"visual_confusion":0.5,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.5}},"aggregated_structure":{"mean_line_fusion_rate":0.0,"mean_line_fragmentation_rate":0.0,"mean_reading_order_score":0.9726,"mean_paragraph_conservation":1.0,"mean_line_accuracy":1.0,"document_count":3},"aggregated_image_quality":{"mean_quality_score":0.7226,"mean_sharpness":0.7032,"mean_noise_level":0.2453,"quality_distribution":{"good":2,"medium":1,"poor":0},"document_count":3,"scores":[0.8262,0.7455,0.5961]},"gini":0.4444,"cer_p90":0.2914,"cer_p99":0.3395,"catastrophic_rate_30":0.1667,"aggregated_line_metrics":{"gini_mean":0.444444,"gini_stdev":0.393818,"mean_cer_mean":0.127874,"percentiles":{"p50":0.083333,"p75":0.211207,"p90":0.291379,"p95":0.318103,"p99":0.339483},"catastrophic_rate":{"0.3":0.166667,"0.5":0.083333,"1.0":0.0},"heatmap":[0.0,0.0,0.0,0.0,0.011494,0.0,0.0,0.166667,0.0,0.333333],"document_count":3},"anchor_score":0.8918,"length_ratio":0.9649,"hallucinating_doc_rate":0.0,"aggregated_hallucination":{"anchor_score_mean":0.891813,"anchor_score_min":0.833333,"length_ratio_mean":0.964912,"net_insertion_rate_mean":0.015873,"hallucinating_doc_count":0,"hallucinating_doc_rate":0.0,"document_count":3},"is_vlm":true}],"documents":[{"doc_id":"folio_001","image_path":"/corpus/images/folio_001.jpg","image_b64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADcCAIAAACOIe9xAAAC4ElEQVR4nO3bsVFDUQxFQZf4y6EIiqAIiiJ0Qu7QCRFg6Z2ZvbMFKDmhbp8f70DUbf0C4NcEDGHPgL/vX0CCgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhbCjgy8yuS8Bm4QnYLLxqwMArCBjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjDPDGZzE7BZeAI2C68aMPAKAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIezfAn4zs79tM2BgnoAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjAP/WanTMBm4W0GDMwTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQ5qHf7JQJ2Cy8zYCBeQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMI89JudMgGbhbcZMDBPwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmId+s1MmYLPwNgMG5gkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAjz0G92ygRsFt5mwMA8AUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYR76zU6ZgM3C2wwYmCdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCHMQ7/ZKROwWXibAQPzBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhHnoNztlAjYLbzNgYJ6AIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwD/1mp0zAZuFtBgzMEzCECRjCfggYyBEwhAkYwh59L5rsdXrQDgAAAABJRU5ErkJggg==","ground_truth":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les croniques de France & d'Angleterre.","mean_cer":0.139,"best_engine":"pero_ocr","engine_results":[{"engine":"pero_ocr","hypothesis":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les croniques de France & d'Angleterre.","cer":0.0,"cer_diplomatic":0.0,"wer":0.0,"duration":0.405,"error":null,"diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les croniques de France & d'Angleterre."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":0,"class_distribution":{},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":1.0,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.5031,"noise_level":0.4962,"rotation_degrees":0.05,"contrast_score":0.6198,"quality_score":0.5875,"quality_tier":"medium","analysis_method":"mock","script_type":"gothique textura"},"line_metrics":{"cer_per_line":[0.0,0.0,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.0,"p90":0.0,"p95":0.0,"p99":0.0},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.0,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.0},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":1.0,"anchor_score":1.0,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":15,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"tesseract","hypothesis":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France 8 d'Angleterre.","cer":0.1158,"cer_diplomatic":0.125,"wer":0.1333,"duration":0.411,"error":null,"diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les"},{"op":"delete","text":"croniques"},{"op":"equal","text":"de France"},{"op":"replace","old":"&","new":"8"},{"op":"equal","text":"d'Angleterre."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":1,"segmentation_error":0,"oov_character":0,"lacuna":1},"total_errors":2,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.5,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.5},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[{"gt":"&","ocr":"8"}],"segmentation_error":[],"oov_character":[],"lacuna":[{"gt":"croniques","ocr":"","position":10}]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.8966,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.6518,"noise_level":0.495,"rotation_degrees":-1.34,"contrast_score":0.2668,"quality_score":0.5284,"quality_tier":"medium","analysis_method":"mock","script_type":"gothique textura"},"line_metrics":{"cer_per_line":[0.0,0.0,0.533333,1.0],"percentiles":{"p50":0.266667,"p75":0.65,"p90":0.86,"p95":0.93,"p99":0.986},"catastrophic_rate":{"0.3":0.5,"0.5":0.5,"1.0":0.0},"gini":0.576087,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.533333,0.0,1.0],"line_count":4,"mean_cer":0.383333},"hallucination_metrics":{"net_insertion_rate":0.071429,"length_ratio":0.894737,"anchor_score":0.666667,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":14,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"ancien_moteur","hypothesis":"Icy commence le de Jehan ſut les croniques de & d'Angleterre.","cer":0.3684,"cer_diplomatic":0.3646,"wer":0.3333,"duration":3.892,"error":null,"diff":[{"op":"equal","text":"Icy commence le"},{"op":"delete","text":"prologue"},{"op":"equal","text":"de"},{"op":"delete","text":"maiſtre"},{"op":"equal","text":"Jehan"},{"op":"replace","old":"Froiſſart ſus","new":"ſut"},{"op":"equal","text":"les croniques de"},{"op":"delete","text":"France"},{"op":"equal","text":"& d'Angleterre."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":1,"oov_character":0,"lacuna":3},"total_errors":4,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.25,"oov_character":0.0,"lacuna":0.75},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[{"gt":"Froiſſart ſus","ocr":"ſut","position":7}],"oov_character":[],"lacuna":[{"gt":"prologue","ocr":"","position":3},{"gt":"maiſtre","ocr":"","position":5},{"gt":"France","ocr":"","position":12}]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.7692,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.1537,"noise_level":0.5589,"rotation_degrees":-2.09,"contrast_score":0.2,"quality_score":0.2888,"quality_tier":"poor","analysis_method":"mock","script_type":"gothique textura"},"line_metrics":{"cer_per_line":[0.433333,0.965517,1.0,1.0],"percentiles":{"p50":0.982759,"p75":1.0,"p90":1.0,"p95":1.0,"p99":1.0},"catastrophic_rate":{"0.3":1.0,"0.5":0.75,"1.0":0.0},"gini":0.127579,"heatmap":[0.0,0.0,0.433333,0.0,0.965517,0.0,0.0,1.0,0.0,1.0],"line_count":4,"mean_cer":0.849713},"hallucination_metrics":{"net_insertion_rate":0.090909,"length_ratio":0.642105,"anchor_score":0.222222,"hallucinated_blocks":[],"is_hallucinating":true,"gt_word_count":15,"hyp_word_count":11,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"tesseract → gpt-4o","hypothesis":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France & d'Angleterre.","cer":0.1053,"cer_diplomatic":0.1042,"wer":0.0667,"duration":11.725,"error":null,"diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les"},{"op":"delete","text":"croniques"},{"op":"equal","text":"de France & d'Angleterre."}],"ocr_intermediate":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France 8 d'Angleterre.","ocr_diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les"},{"op":"delete","text":"croniques"},{"op":"equal","text":"de France"},{"op":"replace","old":"&","new":"8"},{"op":"equal","text":"d'Angleterre."}],"llm_correction_diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France"},{"op":"replace","old":"8","new":"&"},{"op":"equal","text":"d'Angleterre."}],"over_normalization":{"score":0.0,"total_correct_ocr_words":10,"over_normalized_count":0,"over_normalized_passages":[]},"pipeline_mode":"text_and_image","ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":1},"total_errors":1,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":1.0},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[{"gt":"croniques","ocr":"","position":10}]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.9655,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.6971,"noise_level":0.3585,"rotation_degrees":2.94,"contrast_score":0.4231,"quality_score":0.6047,"quality_tier":"medium","analysis_method":"mock","script_type":"gothique textura"},"line_metrics":{"cer_per_line":[0.0,0.0,0.5,1.0],"percentiles":{"p50":0.25,"p75":0.625,"p90":0.85,"p95":0.925,"p99":0.985},"catastrophic_rate":{"0.3":0.5,"0.5":0.25,"1.0":0.0},"gini":0.583333,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,0.0,1.0],"line_count":4,"mean_cer":0.375},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":0.894737,"anchor_score":0.833333,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":14,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"gpt-4o-vision (zero-shot)","hypothesis":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France & d'Angleterre.","cer":0.1053,"cer_diplomatic":0.1042,"wer":0.0667,"duration":5.732,"error":null,"diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les"},{"op":"delete","text":"croniques"},{"op":"equal","text":"de France & d'Angleterre."}],"ocr_intermediate":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France 8 d'Angleterre.","ocr_diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les"},{"op":"delete","text":"croniques"},{"op":"equal","text":"de France"},{"op":"replace","old":"&","new":"8"},{"op":"equal","text":"d'Angleterre."}],"llm_correction_diff":[{"op":"equal","text":"Icy commence le prologue de maiſtre Jehan Froiſſart ſus les de France"},{"op":"replace","old":"8","new":"&"},{"op":"equal","text":"d'Angleterre."}],"over_normalization":{"score":0.0,"total_correct_ocr_words":10,"over_normalized_count":0,"over_normalized_passages":[]},"pipeline_mode":"zero_shot","ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":1},"total_errors":1,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":1.0},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[{"gt":"croniques","ocr":"","position":10}]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.9655,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.8573,"noise_level":0.2091,"rotation_degrees":-2.96,"contrast_score":0.8492,"quality_score":0.8262,"quality_tier":"good","analysis_method":"mock","script_type":"gothique textura"},"line_metrics":{"cer_per_line":[0.0,0.0,0.5,1.0],"percentiles":{"p50":0.25,"p75":0.625,"p90":0.85,"p95":0.925,"p99":0.985},"catastrophic_rate":{"0.3":0.5,"0.5":0.25,"1.0":0.0},"gini":0.583333,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,0.0,1.0],"line_count":4,"mean_cer":0.375},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":0.894737,"anchor_score":0.833333,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":14,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}}],"script_type":"gothique textura","difficulty_score":0.1242,"difficulty_label":"Facile"},{"doc_id":"folio_002","image_path":"/corpus/images/folio_002.jpg","image_b64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADcCAIAAACOIe9xAAAC4ElEQVR4nO3bsVFDUQxFQZf4y6EIiqAIiiJ0Qu7QCRFg6Z2ZvbMFKDmhbp8f70DUbf0C4NcEDGHPgL/vX0CCgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhbCjgy8yuS8Bm4QnYLLxqwMArCBjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjDPDGZzE7BZeAI2C68aMPAKAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIezfAn4zs79tM2BgnoAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjAP/WanTMBm4W0GDMwTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQ5qHf7JQJ2Cy8zYCBeQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMI89JudMgGbhbcZMDBPwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmId+s1MmYLPwNgMG5gkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAjz0G92ygRsFt5mwMA8AUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYR76zU6ZgM3C2wwYmCdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCHMQ7/ZKROwWXibAQPzBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhHnoNztlAjYLbzNgYJ6AIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwD/1mp0zAZuFtBgzMEzCECRjCfggYyBEwhAkYwh59L5rsdXrQDgAAAABJRU5ErkJggg==","ground_truth":"En l'an de grace mil trois cens ſoixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","mean_cer":0.0216,"best_engine":"pero_ocr","engine_results":[{"engine":"pero_ocr","hypothesis":"En l'an de grace mil trois cens ſoixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","cer":0.0,"cer_diplomatic":0.0,"wer":0.0,"duration":0.886,"error":null,"diff":[{"op":"equal","text":"En l'an de grace mil trois cens ſoixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":0,"class_distribution":{},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":1.0,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.6798,"noise_level":0.2595,"rotation_degrees":1.37,"contrast_score":0.8946,"quality_score":0.7747,"quality_tier":"good","analysis_method":"mock","script_type":"humanistique"},"line_metrics":{"cer_per_line":[0.0,0.0,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.0,"p90":0.0,"p95":0.0,"p99":0.0},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.0,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.0},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":1.0,"anchor_score":1.0,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":21,"hyp_word_count":21,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"tesseract","hypothesis":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","cer":0.009,"cer_diplomatic":0.009,"wer":0.0476,"duration":0.971,"error":null,"diff":[{"op":"equal","text":"En l'an de grace mil trois cens"},{"op":"replace","old":"ſoixante,","new":"foixante,"},{"op":"equal","text":"regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":1,"class_distribution":{"visual_confusion":1.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.0},"examples":{"visual_confusion":[{"gt":"ſoixante,","ocr":"foixante,"}],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.9524,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.7507,"noise_level":0.0967,"rotation_degrees":0.68,"contrast_score":0.969,"quality_score":0.8648,"quality_tier":"good","analysis_method":"mock","script_type":"humanistique"},"line_metrics":{"cer_per_line":[0.0,0.034483,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.008621,"p90":0.024138,"p95":0.02931,"p99":0.033448},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.75,"heatmap":[0.0,0.0,0.0,0.0,0.034483,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.008621},"hallucination_metrics":{"net_insertion_rate":0.047619,"length_ratio":1.0,"anchor_score":0.842105,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":21,"hyp_word_count":21,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"ancien_moteur","hypothesis":"l'fn de grwce mil trois cens ſoifante, regnoit en France le noyle roy zehan, filz du row Phelippe de Valois.","cer":0.0811,"cer_diplomatic":0.0811,"wer":0.3333,"duration":2.227,"error":null,"diff":[{"op":"replace","old":"En l'an","new":"l'fn"},{"op":"equal","text":"de"},{"op":"replace","old":"grace","new":"grwce"},{"op":"equal","text":"mil trois cens"},{"op":"replace","old":"ſoixante,","new":"ſoifante,"},{"op":"equal","text":"regnoit en France le"},{"op":"replace","old":"noble","new":"noyle"},{"op":"equal","text":"roy"},{"op":"replace","old":"Jehan,","new":"zehan,"},{"op":"equal","text":"filz du"},{"op":"replace","old":"roy","new":"row"},{"op":"equal","text":"Phelippe de Valois."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":5,"segmentation_error":1,"oov_character":0,"lacuna":0},"total_errors":6,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.8333,"segmentation_error":0.1667,"oov_character":0.0,"lacuna":0.0},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[{"gt":"grace","ocr":"grwce"},{"gt":"ſoixante,","ocr":"ſoifante,"},{"gt":"noble","ocr":"noyle"}],"segmentation_error":[{"gt":"En l'an","ocr":"l'fn","position":0}],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.6829,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.1939,"noise_level":0.5855,"rotation_degrees":-0.28,"contrast_score":0.4345,"quality_score":0.388,"quality_tier":"poor","analysis_method":"mock","script_type":"humanistique"},"line_metrics":{"cer_per_line":[0.266667,0.241379,0.266667,0.142857],"percentiles":{"p50":0.254023,"p75":0.266667,"p90":0.266667,"p95":0.266667,"p99":0.266667},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.108089,"heatmap":[0.0,0.0,0.266667,0.0,0.241379,0.0,0.0,0.266667,0.0,0.142857],"line_count":4,"mean_cer":0.229392},"hallucination_metrics":{"net_insertion_rate":0.3,"length_ratio":0.972973,"anchor_score":0.222222,"hallucinated_blocks":[{"start_token":11,"end_token":16,"text":"noyle roy zehan, filz du row","length":6}],"is_hallucinating":true,"gt_word_count":21,"hyp_word_count":20,"net_inserted_words":6,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"tesseract → gpt-4o","hypothesis":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","cer":0.009,"cer_diplomatic":0.009,"wer":0.0476,"duration":8.963,"error":null,"diff":[{"op":"equal","text":"En l'an de grace mil trois cens"},{"op":"replace","old":"ſoixante,","new":"foixante,"},{"op":"equal","text":"regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"ocr_intermediate":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","ocr_diff":[{"op":"equal","text":"En l'an de grace mil trois cens"},{"op":"replace","old":"ſoixante,","new":"foixante,"},{"op":"equal","text":"regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"llm_correction_diff":[{"op":"equal","text":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"over_normalization":{"score":0.0,"total_correct_ocr_words":20,"over_normalized_count":0,"over_normalized_passages":[]},"pipeline_mode":"text_and_image","ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":1,"class_distribution":{"visual_confusion":1.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.0},"examples":{"visual_confusion":[{"gt":"ſoixante,","ocr":"foixante,"}],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.9524,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.7141,"noise_level":0.3019,"rotation_degrees":0.75,"contrast_score":0.5365,"quality_score":0.6787,"quality_tier":"medium","analysis_method":"mock","script_type":"humanistique"},"line_metrics":{"cer_per_line":[0.0,0.034483,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.008621,"p90":0.024138,"p95":0.02931,"p99":0.033448},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.75,"heatmap":[0.0,0.0,0.0,0.0,0.034483,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.008621},"hallucination_metrics":{"net_insertion_rate":0.047619,"length_ratio":1.0,"anchor_score":0.842105,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":21,"hyp_word_count":21,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"gpt-4o-vision (zero-shot)","hypothesis":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","cer":0.009,"cer_diplomatic":0.009,"wer":0.0476,"duration":11.733,"error":null,"diff":[{"op":"equal","text":"En l'an de grace mil trois cens"},{"op":"replace","old":"ſoixante,","new":"foixante,"},{"op":"equal","text":"regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"ocr_intermediate":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois.","ocr_diff":[{"op":"equal","text":"En l'an de grace mil trois cens"},{"op":"replace","old":"ſoixante,","new":"foixante,"},{"op":"equal","text":"regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"llm_correction_diff":[{"op":"equal","text":"En l'an de grace mil trois cens foixante, regnoit en France le noble roy Jehan, filz du roy Phelippe de Valois."}],"over_normalization":{"score":0.0,"total_correct_ocr_words":20,"over_normalized_count":0,"over_normalized_passages":[]},"pipeline_mode":"zero_shot","ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":1,"class_distribution":{"visual_confusion":1.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.0},"examples":{"visual_confusion":[{"gt":"ſoixante,","ocr":"foixante,"}],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.9524,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.8097,"noise_level":0.0704,"rotation_degrees":-0.49,"contrast_score":0.4687,"quality_score":0.7455,"quality_tier":"good","analysis_method":"mock","script_type":"humanistique"},"line_metrics":{"cer_per_line":[0.0,0.034483,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.008621,"p90":0.024138,"p95":0.02931,"p99":0.033448},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.75,"heatmap":[0.0,0.0,0.0,0.0,0.034483,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.008621},"hallucination_metrics":{"net_insertion_rate":0.047619,"length_ratio":1.0,"anchor_score":0.842105,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":21,"hyp_word_count":21,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}}],"script_type":"humanistique","difficulty_score":0.0973,"difficulty_label":"Facile"},{"doc_id":"folio_003","image_path":"/corpus/images/folio_003.jpg","image_b64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADcCAIAAACOIe9xAAAC4ElEQVR4nO3bsVFDUQxFQZf4y6EIiqAIiiJ0Qu7QCRFg6Z2ZvbMFKDmhbp8f70DUbf0C4NcEDGHPgL/vX0CCgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhbCjgy8yuS8Bm4QnYLLxqwMArCBjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjDPDGZzE7BZeAI2C68aMPAKAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIezfAn4zs79tM2BgnoAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjAP/WanTMBm4W0GDMwTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQ5qHf7JQJ2Cy8zYCBeQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMI89JudMgGbhbcZMDBPwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYQKGMAFDmId+s1MmYLPwNgMG5gkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCFMwBAmYAjz0G92ygRsFt5mwMA8AUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhAkYwgQMYR76zU6ZgM3C2wwYmCdgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwAUOYgCHMQ7/ZKROwWXibAQPzBAxhAoYwAUOYgCFMwBAmYAgTMIQJGMIEDGEChjABQ5iAIUzAECZgCBMwhHnoNztlAjYLbzNgYJ6AIUzAECZgCBMwhAkYwgQMYQKGMAFDmIAhTMAQJmAIEzCECRjCBAxhAoYwD/1mp0zAZuFtBgzMEzCECRjCfggYyBEwhAkYwh59L5rsdXrQDgAAAABJRU5ErkJggg==","ground_truth":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans.","mean_cer":0.02,"best_engine":"pero_ocr","engine_results":[{"engine":"pero_ocr","hypothesis":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans.","cer":0.0,"cer_diplomatic":0.0,"wer":0.0,"duration":2.78,"error":null,"diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":0,"class_distribution":{},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":1.0,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.6592,"noise_level":0.138,"rotation_degrees":-0.22,"contrast_score":1.0,"quality_score":0.8339,"quality_tier":"good","analysis_method":"mock","script_type":"cursive administrative"},"line_metrics":{"cer_per_line":[0.0,0.0,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.0,"p90":0.0,"p95":0.0,"p99":0.0},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.0,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.0},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":1.0,"anchor_score":1.0,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":15,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"tesseract","hypothesis":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins 8 mahommetans.","cer":0.01,"cer_diplomatic":0.0198,"wer":0.0667,"duration":0.69,"error":null,"diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins"},{"op":"replace","old":"&","new":"8"},{"op":"equal","text":"mahommetans."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":1,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":1,"class_distribution":{"visual_confusion":0.0,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":1.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.0},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[{"gt":"&","ocr":"8"}],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.9333,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.7764,"noise_level":0.1395,"rotation_degrees":-0.69,"contrast_score":0.8002,"quality_score":0.8158,"quality_tier":"good","analysis_method":"mock","script_type":"cursive administrative"},"line_metrics":{"cer_per_line":[0.0,0.0,0.033333,0.0],"percentiles":{"p50":0.0,"p75":0.008333,"p90":0.023333,"p95":0.028333,"p99":0.032333},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.75,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.033333,0.0,0.0],"line_count":4,"mean_cer":0.008333},"hallucination_metrics":{"net_insertion_rate":0.066667,"length_ratio":1.0,"anchor_score":0.846154,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":15,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"ancien_moteur","hypothesis":"ledit iour furent menez en ladicte ville Paris pluſieurs priſonniers ſazaſins & mahommetans.","cer":0.09,"cer_diplomatic":0.0891,"wer":0.2,"duration":2.803,"error":null,"diff":[{"op":"delete","text":"Item"},{"op":"equal","text":"ledit iour furent menez en ladicte ville"},{"op":"delete","text":"de"},{"op":"equal","text":"Paris pluſieurs priſonniers"},{"op":"replace","old":"ſaraſins","new":"ſazaſins"},{"op":"equal","text":"& mahommetans."}],"ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":1,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":2},"total_errors":3,"class_distribution":{"visual_confusion":0.3333,"diacritic_error":0.0,"case_error":0.0,"ligature_error":0.0,"abbreviation_error":0.0,"hapax":0.0,"segmentation_error":0.0,"oov_character":0.0,"lacuna":0.6667},"examples":{"visual_confusion":[{"gt":"ſaraſins","ocr":"ſazaſins"}],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[{"gt":"Item","ocr":"","position":0},{"gt":"de","ocr":"","position":8}]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":0.8571,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.9112,"noise_level":0.3059,"rotation_degrees":1.1,"contrast_score":0.5727,"quality_score":0.7641,"quality_tier":"good","analysis_method":"mock","script_type":"cursive administrative"},"line_metrics":{"cer_per_line":[0.333333,0.533333,0.566667,0.8],"percentiles":{"p50":0.55,"p75":0.625,"p90":0.73,"p95":0.765,"p99":0.793},"catastrophic_rate":{"0.3":1.0,"0.5":0.75,"1.0":0.0},"gini":0.160448,"heatmap":[0.0,0.0,0.333333,0.0,0.533333,0.0,0.0,0.566667,0.0,0.8],"line_count":4,"mean_cer":0.558333},"hallucination_metrics":{"net_insertion_rate":0.076923,"length_ratio":0.92,"anchor_score":0.545455,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":13,"net_inserted_words":1,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"tesseract → gpt-4o","hypothesis":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans.","cer":0.0,"cer_diplomatic":0.0,"wer":0.0,"duration":7.601,"error":null,"diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans."}],"ocr_intermediate":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins 8 mahommetans.","ocr_diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins"},{"op":"replace","old":"&","new":"8"},{"op":"equal","text":"mahommetans."}],"llm_correction_diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins"},{"op":"replace","old":"8","new":"&"},{"op":"equal","text":"mahommetans."}],"over_normalization":{"score":0.0,"total_correct_ocr_words":14,"over_normalized_count":0,"over_normalized_passages":[]},"pipeline_mode":"text_and_image","ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":0,"class_distribution":{},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":1.0,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.6989,"noise_level":0.0306,"rotation_degrees":2.4,"contrast_score":0.6456,"quality_score":0.7431,"quality_tier":"good","analysis_method":"mock","script_type":"cursive administrative"},"line_metrics":{"cer_per_line":[0.0,0.0,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.0,"p90":0.0,"p95":0.0,"p99":0.0},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.0,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.0},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":1.0,"anchor_score":1.0,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":15,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}},{"engine":"gpt-4o-vision (zero-shot)","hypothesis":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans.","cer":0.0,"cer_diplomatic":0.0,"wer":0.0,"duration":5.335,"error":null,"diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins & mahommetans."}],"ocr_intermediate":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins 8 mahommetans.","ocr_diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins"},{"op":"replace","old":"&","new":"8"},{"op":"equal","text":"mahommetans."}],"llm_correction_diff":[{"op":"equal","text":"Item ledit iour furent menez en ladicte ville de Paris pluſieurs priſonniers ſaraſins"},{"op":"replace","old":"8","new":"&"},{"op":"equal","text":"mahommetans."}],"over_normalization":{"score":0.0,"total_correct_ocr_words":14,"over_normalized_count":0,"over_normalized_passages":[]},"pipeline_mode":"zero_shot","ligature_score":1.0,"diacritic_score":1.0,"taxonomy":{"counts":{"visual_confusion":0,"diacritic_error":0,"case_error":0,"ligature_error":0,"abbreviation_error":0,"hapax":0,"segmentation_error":0,"oov_character":0,"lacuna":0},"total_errors":0,"class_distribution":{},"examples":{"visual_confusion":[],"diacritic_error":[],"case_error":[],"ligature_error":[],"abbreviation_error":[],"hapax":[],"segmentation_error":[],"oov_character":[],"lacuna":[]}},"structure":{"gt_line_count":1,"ocr_line_count":1,"line_fusion_count":0,"line_fragmentation_count":0,"line_fusion_rate":0.0,"line_fragmentation_rate":0.0,"line_accuracy":1.0,"reading_order_score":1.0,"paragraph_conservation_score":1.0},"image_quality":{"sharpness_score":0.4427,"noise_level":0.4565,"rotation_degrees":1.15,"contrast_score":0.7394,"quality_score":0.5961,"quality_tier":"medium","analysis_method":"mock","script_type":"cursive administrative"},"line_metrics":{"cer_per_line":[0.0,0.0,0.0,0.0],"percentiles":{"p50":0.0,"p75":0.0,"p90":0.0,"p95":0.0,"p99":0.0},"catastrophic_rate":{"0.3":0.0,"0.5":0.0,"1.0":0.0},"gini":0.0,"heatmap":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"line_count":4,"mean_cer":0.0},"hallucination_metrics":{"net_insertion_rate":0.0,"length_ratio":1.0,"anchor_score":1.0,"hallucinated_blocks":[],"is_hallucinating":false,"gt_word_count":15,"hyp_word_count":15,"net_inserted_words":0,"anchor_threshold_used":0.5,"length_ratio_threshold_used":1.2,"ngram_size_used":3}}],"script_type":"cursive administrative","difficulty_score":0.1808,"difficulty_label":"Facile"}],"statistics":{"pairwise_wilcoxon":[{"engine_a":"pero_ocr","engine_b":"tesseract","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le premier concurrent obtient de meilleurs scores.","n_pairs":3,"W_plus":0,"W_minus":6.0},{"engine_a":"pero_ocr","engine_b":"ancien_moteur","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le premier concurrent obtient de meilleurs scores.","n_pairs":3,"W_plus":0,"W_minus":6.0},{"engine_a":"pero_ocr","engine_b":"tesseract → gpt-4o","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le premier concurrent obtient de meilleurs scores.","n_pairs":2,"W_plus":0,"W_minus":3.0},{"engine_a":"pero_ocr","engine_b":"gpt-4o-vision (zero-shot)","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le premier concurrent obtient de meilleurs scores.","n_pairs":2,"W_plus":0,"W_minus":3.0},{"engine_a":"tesseract","engine_b":"ancien_moteur","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le premier concurrent obtient de meilleurs scores.","n_pairs":3,"W_plus":0,"W_minus":6.0},{"engine_a":"tesseract","engine_b":"tesseract → gpt-4o","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le second concurrent obtient de meilleurs scores.","n_pairs":2,"W_plus":3.0,"W_minus":0},{"engine_a":"tesseract","engine_b":"gpt-4o-vision (zero-shot)","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le second concurrent obtient de meilleurs scores.","n_pairs":2,"W_plus":3.0,"W_minus":0},{"engine_a":"ancien_moteur","engine_b":"tesseract → gpt-4o","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le second concurrent obtient de meilleurs scores.","n_pairs":3,"W_plus":6.0,"W_minus":0},{"engine_a":"ancien_moteur","engine_b":"gpt-4o-vision (zero-shot)","statistic":0,"p_value":0.04,"significant":true,"interpretation":"Différence statistiquement significative (p = 0.0400 < 0.05). Le second concurrent obtient de meilleurs scores.","n_pairs":3,"W_plus":6.0,"W_minus":0},{"engine_a":"tesseract → gpt-4o","engine_b":"gpt-4o-vision (zero-shot)","statistic":0.0,"p_value":1.0,"significant":false,"interpretation":"Aucune différence entre les deux concurrents.","n_pairs":0}],"bootstrap_cis":[{"engine":"pero_ocr","mean":0.0,"ci_lower":0.0,"ci_upper":0.0},{"engine":"tesseract","mean":0.0449,"ci_lower":0.009,"ci_upper":0.1158},{"engine":"ancien_moteur","mean":0.1798,"ci_lower":0.0811,"ci_upper":0.3684},{"engine":"tesseract → gpt-4o","mean":0.0381,"ci_lower":0.0,"ci_upper":0.1053},{"engine":"gpt-4o-vision (zero-shot)","mean":0.0381,"ci_lower":0.0,"ci_upper":0.1053}]},"reliability_curves":[{"engine":"pero_ocr","points":[{"pct_docs":5.0,"mean_cer":0.0},{"pct_docs":10.0,"mean_cer":0.0},{"pct_docs":15.0,"mean_cer":0.0},{"pct_docs":20.0,"mean_cer":0.0},{"pct_docs":25.0,"mean_cer":0.0},{"pct_docs":30.0,"mean_cer":0.0},{"pct_docs":35.0,"mean_cer":0.0},{"pct_docs":40.0,"mean_cer":0.0},{"pct_docs":45.0,"mean_cer":0.0},{"pct_docs":50.0,"mean_cer":0.0},{"pct_docs":55.0,"mean_cer":0.0},{"pct_docs":60.0,"mean_cer":0.0},{"pct_docs":65.0,"mean_cer":0.0},{"pct_docs":70.0,"mean_cer":0.0},{"pct_docs":75.0,"mean_cer":0.0},{"pct_docs":80.0,"mean_cer":0.0},{"pct_docs":85.0,"mean_cer":0.0},{"pct_docs":90.0,"mean_cer":0.0},{"pct_docs":95.0,"mean_cer":0.0},{"pct_docs":100.0,"mean_cer":0.0}]},{"engine":"tesseract","points":[{"pct_docs":5.0,"mean_cer":0.009},{"pct_docs":10.0,"mean_cer":0.009},{"pct_docs":15.0,"mean_cer":0.009},{"pct_docs":20.0,"mean_cer":0.009},{"pct_docs":25.0,"mean_cer":0.009},{"pct_docs":30.0,"mean_cer":0.009},{"pct_docs":35.0,"mean_cer":0.009},{"pct_docs":40.0,"mean_cer":0.009},{"pct_docs":45.0,"mean_cer":0.009},{"pct_docs":50.0,"mean_cer":0.009},{"pct_docs":55.0,"mean_cer":0.009},{"pct_docs":60.0,"mean_cer":0.009},{"pct_docs":65.0,"mean_cer":0.009},{"pct_docs":70.0,"mean_cer":0.0095},{"pct_docs":75.0,"mean_cer":0.0095},{"pct_docs":80.0,"mean_cer":0.0095},{"pct_docs":85.0,"mean_cer":0.0095},{"pct_docs":90.0,"mean_cer":0.0095},{"pct_docs":95.0,"mean_cer":0.0095},{"pct_docs":100.0,"mean_cer":0.044933}]},{"engine":"ancien_moteur","points":[{"pct_docs":5.0,"mean_cer":0.0811},{"pct_docs":10.0,"mean_cer":0.0811},{"pct_docs":15.0,"mean_cer":0.0811},{"pct_docs":20.0,"mean_cer":0.0811},{"pct_docs":25.0,"mean_cer":0.0811},{"pct_docs":30.0,"mean_cer":0.0811},{"pct_docs":35.0,"mean_cer":0.0811},{"pct_docs":40.0,"mean_cer":0.0811},{"pct_docs":45.0,"mean_cer":0.0811},{"pct_docs":50.0,"mean_cer":0.0811},{"pct_docs":55.0,"mean_cer":0.0811},{"pct_docs":60.0,"mean_cer":0.0811},{"pct_docs":65.0,"mean_cer":0.0811},{"pct_docs":70.0,"mean_cer":0.08555},{"pct_docs":75.0,"mean_cer":0.08555},{"pct_docs":80.0,"mean_cer":0.08555},{"pct_docs":85.0,"mean_cer":0.08555},{"pct_docs":90.0,"mean_cer":0.08555},{"pct_docs":95.0,"mean_cer":0.08555},{"pct_docs":100.0,"mean_cer":0.179833}]},{"engine":"tesseract → gpt-4o","points":[{"pct_docs":5.0,"mean_cer":0.0},{"pct_docs":10.0,"mean_cer":0.0},{"pct_docs":15.0,"mean_cer":0.0},{"pct_docs":20.0,"mean_cer":0.0},{"pct_docs":25.0,"mean_cer":0.0},{"pct_docs":30.0,"mean_cer":0.0},{"pct_docs":35.0,"mean_cer":0.0},{"pct_docs":40.0,"mean_cer":0.0},{"pct_docs":45.0,"mean_cer":0.0},{"pct_docs":50.0,"mean_cer":0.0},{"pct_docs":55.0,"mean_cer":0.0},{"pct_docs":60.0,"mean_cer":0.0},{"pct_docs":65.0,"mean_cer":0.0},{"pct_docs":70.0,"mean_cer":0.0045},{"pct_docs":75.0,"mean_cer":0.0045},{"pct_docs":80.0,"mean_cer":0.0045},{"pct_docs":85.0,"mean_cer":0.0045},{"pct_docs":90.0,"mean_cer":0.0045},{"pct_docs":95.0,"mean_cer":0.0045},{"pct_docs":100.0,"mean_cer":0.0381}]},{"engine":"gpt-4o-vision (zero-shot)","points":[{"pct_docs":5.0,"mean_cer":0.0},{"pct_docs":10.0,"mean_cer":0.0},{"pct_docs":15.0,"mean_cer":0.0},{"pct_docs":20.0,"mean_cer":0.0},{"pct_docs":25.0,"mean_cer":0.0},{"pct_docs":30.0,"mean_cer":0.0},{"pct_docs":35.0,"mean_cer":0.0},{"pct_docs":40.0,"mean_cer":0.0},{"pct_docs":45.0,"mean_cer":0.0},{"pct_docs":50.0,"mean_cer":0.0},{"pct_docs":55.0,"mean_cer":0.0},{"pct_docs":60.0,"mean_cer":0.0},{"pct_docs":65.0,"mean_cer":0.0},{"pct_docs":70.0,"mean_cer":0.0045},{"pct_docs":75.0,"mean_cer":0.0045},{"pct_docs":80.0,"mean_cer":0.0045},{"pct_docs":85.0,"mean_cer":0.0045},{"pct_docs":90.0,"mean_cer":0.0045},{"pct_docs":95.0,"mean_cer":0.0045},{"pct_docs":100.0,"mean_cer":0.0381}]}],"venn_data":{"type":"venn3","label_a":"pero_ocr","label_b":"tesseract","label_c":"ancien_moteur","only_a":0,"only_b":4,"only_c":13,"ab":0,"ac":0,"bc":0,"abc":0},"error_clusters":[{"cluster_id":5,"label":"autres substitutions","count":15,"examples":[{"engine":"tesseract","gt_fragment":"croniques","ocr_fragment":""},{"engine":"tesseract","gt_fragment":"ſoixante,","ocr_fragment":"foixante,"},{"engine":"ancien_moteur","gt_fragment":"prologue","ocr_fragment":""},{"engine":"ancien_moteur","gt_fragment":"maiſtre","ocr_fragment":""},{"engine":"ancien_moteur","gt_fragment":"France","ocr_fragment":""}]},{"cluster_id":1,"label":"&→8","count":2,"examples":[{"engine":"tesseract","gt_fragment":"&","ocr_fragment":"8"},{"engine":"tesseract","gt_fragment":"&","ocr_fragment":"8"}]},{"cluster_id":2,"label":"confusion ſ/f/s","count":2,"examples":[{"engine":"ancien_moteur","gt_fragment":"Froiſſart ſus","ocr_fragment":"ſut"},{"engine":"ancien_moteur","gt_fragment":"ſaraſins","ocr_fragment":"ſazaſins"}]},{"cluster_id":3,"label":"roy→row","count":1,"examples":[{"engine":"ancien_moteur","gt_fragment":"roy","ocr_fragment":"row"}]},{"cluster_id":4,"label":"de→—","count":1,"examples":[{"engine":"ancien_moteur","gt_fragment":"de","ocr_fragment":""}]}],"correlation_per_engine":[{"engine":"pero_ocr","labels":["cer","wer","mer","wil","quality_score","sharpness","ligature","diacritic"],"matrix":[[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,1.0,0.9431,0.0,0.0],[0.0,0.0,0.0,0.0,0.9431,1.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]]},{"engine":"tesseract","labels":["cer","wer","mer","wil","quality_score","sharpness","ligature","diacritic"],"matrix":[[1.0,0.9789,0.9789,0.9409,-0.9919,-0.9791,0.0,0.0],[0.9789,1.0,1.0,0.9903,-0.9969,-0.9169,0.0,0.0],[0.9789,1.0,1.0,0.9903,-0.9969,-0.9169,0.0,0.0],[0.9409,0.9903,0.9903,1.0,-0.9763,-0.8525,0.0,0.0],[-0.9919,-0.9969,-0.9969,-0.9763,1.0,0.9455,0.0,0.0],[-0.9791,-0.9169,-0.9169,-0.8525,0.9455,1.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]]},{"engine":"ancien_moteur","labels":["cer","wer","mer","wil","quality_score","sharpness","ligature","diacritic"],"matrix":[[1.0,0.4762,0.4762,-0.0421,-0.6408,-0.5172,0.0,0.0],[0.4762,1.0,1.0,0.8585,-0.9802,-0.9989,0.0,0.0],[0.4762,1.0,1.0,0.8585,-0.9802,-0.9989,0.0,0.0],[-0.0421,0.8585,0.8585,1.0,-0.7401,-0.8334,0.0,0.0],[-0.6408,-0.9802,-0.9802,-0.7401,1.0,0.9885,0.0,0.0],[-0.5172,-0.9989,-0.9989,-0.8334,0.9885,1.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]]},{"engine":"tesseract → gpt-4o","labels":["cer","wer","mer","wil","quality_score","sharpness","ligature","diacritic"],"matrix":[[1.0,0.7723,0.7723,0.3173,-0.9185,-0.5167,0.0,0.0],[0.7723,1.0,1.0,0.8475,-0.9605,0.1448,0.0,0.0],[0.7723,1.0,1.0,0.8475,-0.9605,0.1448,0.0,0.0],[0.3173,0.8475,0.8475,1.0,-0.6664,0.648,0.0,0.0],[-0.9185,-0.9605,-0.9605,-0.6664,1.0,0.1361,0.0,0.0],[-0.5167,0.1448,0.1448,0.648,0.1361,1.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]]},{"engine":"gpt-4o-vision (zero-shot)","labels":["cer","wer","mer","wil","quality_score","sharpness","ligature","diacritic"],"matrix":[[1.0,0.7723,0.7723,0.3173,0.8155,0.6487,0.0,0.0],[0.7723,1.0,1.0,0.8475,0.9975,0.9844,0.0,0.0],[0.7723,1.0,1.0,0.8475,0.9975,0.9844,0.0,0.0],[0.3173,0.8475,0.8475,1.0,0.8076,0.9276,0.0,0.0],[0.8155,0.9975,0.9975,0.8076,1.0,0.9695,0.0,0.0],[0.6487,0.9844,0.9844,0.9276,0.9695,1.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]]}],"gini_vs_cer":[{"engine":"pero_ocr","cer":0.0,"gini":0.0,"is_pipeline":false},{"engine":"tesseract","cer":0.0449,"gini":0.692,"is_pipeline":false},{"engine":"ancien_moteur","cer":0.1798,"gini":0.132,"is_pipeline":false},{"engine":"tesseract → gpt-4o","cer":0.0381,"gini":0.4444,"is_pipeline":true},{"engine":"gpt-4o-vision (zero-shot)","cer":0.0381,"gini":0.4444,"is_pipeline":true}],"ratio_vs_anchor":[{"engine":"pero_ocr","length_ratio":1.0,"anchor_score":1.0,"hallucinating_rate":0.0,"is_vlm":false},{"engine":"tesseract","length_ratio":0.9649,"anchor_score":0.785,"hallucinating_rate":0.0,"is_vlm":false},{"engine":"ancien_moteur","length_ratio":0.845,"anchor_score":0.33,"hallucinating_rate":0.6667,"is_vlm":false},{"engine":"tesseract → gpt-4o","length_ratio":0.9649,"anchor_score":0.8918,"hallucinating_rate":0.0,"is_vlm":false},{"engine":"gpt-4o-vision (zero-shot)","length_ratio":0.9649,"anchor_score":0.8918,"hallucinating_rate":0.0,"is_vlm":true}]};
const I18N = {"html_lang":"fr","date_locale":"fr-FR","nav_report":"rapport OCR","tab_ranking":"Classement","tab_gallery":"Galerie","tab_document":"Document","tab_characters":"Caractères","tab_analyses":"Analyses","btn_present":"⊞ Présentation","h_ranking":"Classement des moteurs","col_rank":"#","col_engine":"Concurrent","col_cer":"CER exact","col_cer_diplo":"CER diplo.","col_cer_diplo_title":"CER après normalisation diplomatique (ſ=s, u=v, i=j…) — mesure les erreurs substantielles en ignorant les variantes graphiques codifiées","col_wer":"WER","col_mer":"MER","col_wil":"WIL","col_ligatures":"Ligatures","col_ligatures_title":"Taux de reconnaissance des ligatures (fi, fl, œ, æ, ff…)","col_diacritics":"Diacritiques","col_diacritics_title":"Taux de conservation des diacritiques (accents, cédilles, trémas…)","col_gini":"Gini","col_gini_title":"Coefficient de Gini des erreurs CER par ligne — 0 = erreurs uniformes, 1 = erreurs concentrées. Un bon moteur a CER bas ET Gini bas.","col_anchor":"Ancrage","col_anchor_title":"Score d'ancrage : proportion des trigrammes de la sortie trouvant un ancrage dans le GT — faible score = hallucinations probables (LLM/VLM)","col_cer_median":"CER médian","col_cer_min":"CER min","col_cer_max":"CER max","col_overnorm":"Sur-norm.","col_overnorm_title":"Classe 10 — Sur-normalisation LLM : taux de mots corrects dégradés par le LLM","col_docs":"Docs","h_gallery":"Galerie des documents","gallery_sort_label":"Trier par :","gallery_sort_id":"Identifiant","gallery_sort_cer":"CER moyen","gallery_sort_difficulty":"Difficulté","gallery_sort_best":"Meilleur moteur","gallery_filter_cer_label":"Filtrer CER >","gallery_filter_engine_label":"Moteur :","gallery_filter_all":"Tous","gallery_empty":"Aucun document ne correspond aux filtres.","doc_sidebar_header":"Documents","doc_title_default":"Sélectionner un document","h_image":"Image originale","h_gt":"Vérité terrain (GT)","h_diff":"Sorties OCR — diff par moteur","h_line_metrics":"Distribution des erreurs par ligne","h_hallucination":"Analyse des hallucinations","h_characters":"Analyse des caractères","char_engine_label":"Moteur :","h_cer_dist":"Distribution du CER par moteur","h_radar":"Profil des moteurs (radar)","radar_note":"Axe radar : CER, WER, MER, WIL — valeurs inversées (plus c'est haut, meilleur est le moteur).","h_cer_doc":"CER par document (tous moteurs)","h_duration":"Temps d'exécution moyen (secondes/document)","h_quality_cer":"Qualité image ↔ CER (scatter plot)","quality_cer_note":"Chaque point = un document. Axe X = score qualité image [0–1]. Axe Y = CER. Corrélation négative attendue.","h_taxonomy":"Taxonomie des erreurs par moteur","taxonomy_note":"Distribution des classes d'erreurs (classes 1–9 de la taxonomie Picarones).","h_reliability":"Courbes de fiabilité","reliability_note":"Pour les X% documents les plus faciles (triés par CER croissant), quel est le CER moyen cumulé ? Une courbe basse = moteur performant même sur les documents faciles.","h_bootstrap":"Intervalles de confiance à 95 % (bootstrap)","bootstrap_note":"IC à 95% sur le CER moyen par moteur (1000 itérations bootstrap).","h_venn":"Erreurs communes / exclusives (Venn)","venn_note":"Intersection des ensembles d'erreurs entre les 2 ou 3 premiers concurrents. Erreurs communes = segments partagés.","h_pairwise":"Tests de Wilcoxon — comparaisons par paires","pairwise_note":"Test signé-rangé de Wilcoxon (non-paramétrique). Seuil α = 0.05.","h_clusters":"Clustering des patterns d'erreurs","h_gini_cer":"Gini vs CER moyen","gini_cer_ideal":"— idéal : bas-gauche","gini_cer_note":"Axe X = CER moyen, Axe Y = coefficient de Gini. Un moteur idéal a CER bas ET Gini bas (erreurs rares et uniformes).","h_ratio_anchor":"Ratio longueur vs ancrage","ratio_anchor_subtitle":"— hallucinations VLM","ratio_anchor_note":"Axe X = score d'ancrage trigrammes [0–1]. Axe Y = ratio longueur sortie/GT. Zone ⚠️ : ancrage &lt; 0.5 ou ratio &gt; 1.2 → hallucinations probables.","h_correlation":"Matrice de corrélation entre métriques","corr_engine_label":"Moteur :","corr_note":"Coefficient de Pearson entre les métriques CER, WER, qualité image, ligatures, diacritiques. Vert = corrélation positive, Rouge = corrélation négative.","footer_generated":"Rapport généré le","footer_by":"par Picarones","heatmap_start":"Début","heatmap_mid":"Milieu","heatmap_end":"Fin","heatmap_title":"CARTE THERMIQUE (position)","percentile_title":"PERCENTILES CER","lines":"lignes","no_line_metrics":"Aucune métrique de ligne disponible.","no_hall_metrics":"Aucune métrique d'hallucination disponible.","no_hall_blocks":"Aucun bloc halluciné détecté.","hall_detected":"⚠️ Hallucinations détectées","hall_ok":"✓ Ancrage satisfaisant","hall_blocks_title":"Blocs sans ancrage dans le GT :","hall_block_label":"Bloc halluciné","hall_more_blocks":"bloc(s) supplémentaire(s)","no_gini":"Données Gini non disponibles.","no_scatter":"Données non disponibles.","total_errors":"Total :","errors_classified":"erreurs classifiées.","class_col":"Classe","proportion_col":"Proportion","taxonomy_engine_label":"Moteur :"};
</script>
<!-- ── Application ────────────────────────────────────────────────── -->
<script>
'use strict';
// ── Palette couleurs par moteur ──────────────────────────────────
const PALETTE = [
'#2563eb','#dc2626','#16a34a','#ca8a04','#7c3aed',
'#0891b2','#c2410c','#0f766e','#9333ea','#b45309',
];
function engineColor(idx) { return PALETTE[idx % PALETTE.length]; }
// ── Navigation ──────────────────────────────────────────────────
let currentView = 'ranking';
function showView(name) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('view-' + name).classList.add('active');
// Activer le bon onglet nav
const tabMap = {ranking:'classement',gallery:'galerie',document:'document',characters:'caract',analyses:'analyses'};
const prefix = tabMap[name] || name;
document.querySelectorAll('.tab-btn').forEach(b => {
if (b.textContent.toLowerCase().startsWith(prefix.toLowerCase())) b.classList.add('active');
});
currentView = name;
if (name === 'analyses' && !chartsBuilt) buildCharts();
if (name === 'characters' && !charViewBuilt) initCharView();
}
// ── Formatage ───────────────────────────────────────────────────
function pct(v, d=2) {
if (v === null || v === undefined) return '—';
return (v * 100).toFixed(d) + ' %';
}
function cerColor(v) {
if (v < 0.05) return '#16a34a';
if (v < 0.15) return '#ca8a04';
if (v < 0.30) return '#ea580c';
return '#dc2626';
}
function cerBg(v) {
if (v < 0.05) return '#dcfce7';
if (v < 0.15) return '#fef9c3';
if (v < 0.30) return '#ffedd5';
return '#fee2e2';
}
function esc(s) {
return String(s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Diff renderer ──────────────────────────────────────────────
function renderDiff(ops) {
if (!ops || !ops.length) return '<em style="color:var(--text-muted)">— aucune sortie —</em>';
return ops.map(op => {
if (op.op === 'equal')
return '<span class="d-eq">' + esc(op.text) + '</span>';
if (op.op === 'insert')
return '<span class="d-ins" title="Insertion OCR">' + esc(op.text) + '</span>';
if (op.op === 'delete')
return '<span class="d-del" title="Suppression (présent GT)">' + esc(op.text) + '</span>';
if (op.op === 'replace')
return '<span class="d-rep-old" title="Remplacement">' + esc(op.old) + '</span>'
+ '<span class="d-rep-new">' + esc(op.new) + '</span>';
return '';
}).join(' ');
}
// ── Score badge (ligatures / diacritiques) ───────────────────────
function _scoreBadge(v, label) {
if (v === null || v === undefined) return '<span style="color:var(--text-muted)"></span>';
const pctVal = (v * 100).toFixed(1);
const color = v >= 0.9 ? '#16a34a' : v >= 0.7 ? '#ca8a04' : '#dc2626';
const bg = v >= 0.9 ? '#f0fdf4' : v >= 0.7 ? '#fefce8' : '#fef2f2';
return `<span class="cer-badge" style="color:${color};background:${bg}" title="${label} : ${pctVal}%">${pctVal}%</span>`;
}
// ── Vue Classement ──────────────────────────────────────────────
let rankingSort = { col: 'cer', dir: 'asc' };
function renderRanking() {
const engines = [...DATA.engines];
// Trier
engines.sort((a, b) => {
let va = a[rankingSort.col], vb = b[rankingSort.col];
if (typeof va === 'string') va = va.toLowerCase();
if (typeof vb === 'string') vb = vb.toLowerCase();
if (va === null) va = Infinity;
if (vb === null) vb = Infinity;
return rankingSort.dir === 'asc' ? (va > vb ? 1 : -1) : (va < vb ? 1 : -1);
});
const tbody = document.getElementById('ranking-tbody');
tbody.innerHTML = engines.map((e, i) => {
const rank = i + 1;
const badgeClass = rank === 1 ? 'rank-badge rank-1' : 'rank-badge';
const cerC = cerColor(e.cer); const cerB = cerBg(e.cer);
const barW = Math.min(100, e.cer * 100 * 3);
// Badge pipeline
let pipelineBadge = '';
let pipelineStepsHtml = '';
if (e.is_pipeline && e.pipeline_info) {
const pi = e.pipeline_info;
const modeLabel = {text_only:'texte', text_and_image:'image+texte', zero_shot:'zero-shot'}[pi.pipeline_mode] || pi.pipeline_mode || '';
pipelineBadge = `<span class="pipeline-tag" title="Pipeline OCR+LLM — mode ${modeLabel}">
⛓ pipeline<span class="pipe-arrow">·${modeLabel}</span></span>`;
if (pi.pipeline_steps) {
pipelineStepsHtml = `<div class="pipeline-steps">` +
pi.pipeline_steps.map(s => s.type === 'ocr'
? `<span class="step-chip ocr">OCR: ${esc(s.engine)}</span>`
: `<span class="step-chip llm">LLM: ${esc(s.model)}</span>`
).join(`<span class="step-arrow"></span>`) +
`</div>`;
}
}
// Sur-normalisation (classe 10)
let overNormCell = '<td style="color:var(--text-muted)"></td>';
if (e.is_pipeline && e.pipeline_info && e.pipeline_info.over_normalization) {
const on = e.pipeline_info.over_normalization;
const onPct = (on.score * 100).toFixed(2);
const cls = on.score > 0.05 ? 'over-norm-badge high' : 'over-norm-badge';
overNormCell = `<td><span class="${cls}" title="Classe 10 — ${on.over_normalized_count} mots corrects dégradés sur ${on.total_correct_ocr_words}">${onPct} %</span></td>`;
}
// CER diplomatique
let diploCerCell = '<td style="color:var(--text-muted)"></td>';
if (e.cer_diplomatic !== null && e.cer_diplomatic !== undefined) {
const dipC = cerColor(e.cer_diplomatic); const dipB = cerBg(e.cer_diplomatic);
const delta = e.cer - e.cer_diplomatic;
const deltaStr = delta > 0.001 ? ` <span style="font-size:.65rem;color:#059669">-${(delta*100).toFixed(1)}%</span>` : '';
const profileHint = e.cer_diplomatic_profile ? ` title="Profil : ${esc(e.cer_diplomatic_profile)}"` : '';
diploCerCell = `<td${profileHint}>
<span class="cer-badge" style="color:${dipC};background:${dipB}">${pct(e.cer_diplomatic)}</span>${deltaStr}
</td>`;
}
// ── Sprint 10 : Gini + Ancrage ─────────────────────────────────────
let giniCell = '<td style="color:var(--text-muted)"></td>';
if (e.gini !== null && e.gini !== undefined) {
const gv = e.gini;
const gColor = gv < 0.3 ? '#16a34a' : gv < 0.5 ? '#ca8a04' : '#dc2626';
const gBg = gv < 0.3 ? '#f0fdf4' : gv < 0.5 ? '#fefce8' : '#fef2f2';
giniCell = `<td><span class="cer-badge" style="color:${gColor};background:${gBg}"
title="Gini=${gv.toFixed(3)} — 0=uniforme, 1=concentré">${gv.toFixed(3)}</span></td>`;
}
let anchorCell = '<td style="color:var(--text-muted)"></td>';
if (e.anchor_score !== null && e.anchor_score !== undefined) {
const av = e.anchor_score;
const hallBadge = (e.hallucinating_doc_rate && e.hallucinating_doc_rate > 0.2)
? ' <span title="Hallucinations détectées">⚠️</span>' : '';
anchorCell = `<td>${_scoreBadge(av, 'Ancrage trigrammes')}${hallBadge}</td>`;
}
return `<tr>
<td><span class="${badgeClass}">${rank}</span></td>
<td>
<span class="engine-name">${esc(e.name)}</span>
${pipelineBadge}
${e.is_vlm ? '<span class="pipeline-tag" style="background:#fce7f3;color:#9d174d">👁 VLM</span>' : ''}
<span class="engine-version">v${esc(e.version)}</span>
${pipelineStepsHtml}
</td>
<td>
<span class="bar" style="width:${barW}px;background:${cerC}"></span>
<span class="cer-badge" style="color:${cerC};background:${cerB}">${pct(e.cer)}</span>
</td>
${diploCerCell}
<td>${pct(e.wer)}</td>
<td>${pct(e.mer)}</td>
<td>${pct(e.wil)}</td>
<td>${_scoreBadge(e.ligature_score, 'Ligatures')}</td>
<td>${_scoreBadge(e.diacritic_score, 'Diacritiques')}</td>
${giniCell}
${anchorCell}
<td style="color:var(--text-muted)">${pct(e.cer_median)}</td>
<td style="color:var(--text-muted)">${pct(e.cer_min)}</td>
<td style="color:var(--text-muted)">${pct(e.cer_max)}</td>
${overNormCell}
<td><span class="pill">${e.doc_count}</span></td>
</tr>`;
}).join('');
// Stats globales
const pipelineCount = DATA.engines.filter(e => e.is_pipeline).length;
const stats = document.getElementById('ranking-stats');
stats.innerHTML = `
<div class="stat">Corpus <b>${esc(DATA.meta.corpus_name)}</b></div>
<div class="stat">Documents <b>${DATA.meta.document_count}</b></div>
<div class="stat">Concurrents <b>${DATA.engines.length}</b>
${pipelineCount ? `<span class="pipeline-tag" style="margin-left:.3rem">${pipelineCount} pipeline${pipelineCount>1?'s':''}</span>` : ''}
</div>
`;
}
// Tri au clic sur en-tête
document.querySelectorAll('#ranking-table th.sortable').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.col;
if (rankingSort.col === col) {
rankingSort.dir = rankingSort.dir === 'asc' ? 'desc' : 'asc';
} else {
rankingSort.col = col;
rankingSort.dir = 'asc';
}
document.querySelectorAll('#ranking-table th').forEach(t => {
t.classList.remove('sorted');
const icon = t.querySelector('.sort-icon');
if (icon) icon.textContent = '↕';
});
th.classList.add('sorted');
const icon = th.querySelector('.sort-icon');
if (icon) icon.textContent = rankingSort.dir === 'asc' ? '↑' : '↓';
renderRanking();
});
});
// ── Vue Galerie ─────────────────────────────────────────────────
function renderGallery() {
const sortKey = document.getElementById('gallery-sort').value;
const filterCer = parseFloat(document.getElementById('gallery-filter-cer').value) / 100 || 0;
const filterEngine = document.getElementById('gallery-engine-select').value;
let docs = [...DATA.documents];
// Filtre CER
if (filterCer > 0) {
docs = docs.filter(d => {
if (filterEngine) {
const er = d.engine_results.find(r => r.engine === filterEngine);
return er && er.cer >= filterCer;
}
return d.mean_cer >= filterCer;
});
}
// Tri
docs.sort((a, b) => {
if (sortKey === 'mean_cer') return a.mean_cer - b.mean_cer;
if (sortKey === 'difficulty_score') return (b.difficulty_score||0) - (a.difficulty_score||0);
if (sortKey === 'best_engine') return a.best_engine.localeCompare(b.best_engine);
return a.doc_id.localeCompare(b.doc_id);
});
const grid = document.getElementById('gallery-grid');
const empty = document.getElementById('gallery-empty');
if (!docs.length) {
grid.innerHTML = '';
empty.style.display = '';
return;
}
empty.style.display = 'none';
grid.innerHTML = docs.map(doc => {
const imgTag = doc.image_b64
? `<img src="${doc.image_b64}" alt="${esc(doc.doc_id)}" loading="lazy">`
: `<div class="img-placeholder">🖹</div>`;
const badges = doc.engine_results.map(er => {
const c = cerColor(er.cer); const bg = cerBg(er.cer);
const isPipe = er.ocr_intermediate !== undefined;
const label = isPipe ? '⛓' + er.engine.slice(0,8) : er.engine.slice(0,8);
return `<span class="engine-cer-badge" style="color:${c};background:${bg}"
title="${esc(er.engine)}${isPipe?' (pipeline)':''}">${esc(label)} ${pct(er.cer,1)}</span>`;
}).join('');
// Difficulty badge
let diffBadge = '';
if (doc.difficulty_score !== undefined) {
const dScore = doc.difficulty_score;
const dColor = dScore < 0.25 ? '#16a34a' : dScore < 0.5 ? '#ca8a04' : dScore < 0.75 ? '#ea580c' : '#dc2626';
const dBg = dScore < 0.25 ? '#f0fdf4' : dScore < 0.5 ? '#fefce8' : dScore < 0.75 ? '#fff7ed' : '#fef2f2';
diffBadge = `<span class="diff-badge" style="color:${dColor};background:${dBg};margin-left:.3rem"
title="Difficulté intrinsèque : ${doc.difficulty_label}">⚡ ${doc.difficulty_label}</span>`;
}
return `<div class="gallery-card" onclick="openDocument('${esc(doc.doc_id)}')">
${imgTag}
<div class="gallery-card-body">
<div class="gallery-card-title">${esc(doc.doc_id)}${diffBadge}</div>
<div class="gallery-card-badges">${badges}</div>
</div>
</div>`;
}).join('');
}
// ── Vue Document ────────────────────────────────────────────────
let currentDocId = null;
let zoomLevel = 1;
let dragStart = null;
let imgOffset = { x: 0, y: 0 };
function openDocument(docId) {
_origShowView('document');
updateURL('document', { doc: docId });
loadDocument(docId);
}
function loadDocument(docId) {
const doc = DATA.documents.find(d => d.doc_id === docId);
if (!doc) return;
currentDocId = docId;
// Sidebar : highlight
document.querySelectorAll('.doc-list-item').forEach(el => {
el.classList.toggle('active', el.dataset.docId === docId);
});
// Titre
document.getElementById('doc-detail-title').textContent = doc.doc_id;
// Métriques
const metricsDiv = document.getElementById('doc-detail-metrics');
const cer = doc.mean_cer;
const dScore = doc.difficulty_score;
const dColor = dScore < 0.25 ? '#16a34a' : dScore < 0.5 ? '#ca8a04' : dScore < 0.75 ? '#ea580c' : '#dc2626';
const dLabel = doc.difficulty_label || '';
metricsDiv.innerHTML = `<div class="stat">CER moyen <b style="color:${cerColor(cer)}">${pct(cer)}</b></div>
<div class="stat">Meilleur moteur <b>${esc(doc.best_engine)}</b></div>
${dScore !== undefined ? `<div class="stat">Difficulté <b style="color:${dColor}">${dLabel} (${(dScore*100).toFixed(0)}%)</b></div>` : ''}`;
// Image
resetZoom();
const img = document.getElementById('doc-image');
const placeholder = document.getElementById('doc-image-placeholder');
if (doc.image_b64) {
img.src = doc.image_b64;
img.style.display = '';
placeholder.style.display = 'none';
} else {
img.style.display = 'none';
placeholder.style.display = '';
placeholder.innerHTML = `<span style="font-size:2rem">🖹</span><span>${esc(doc.image_path)}</span>`;
}
// GT
document.getElementById('doc-gt-text').textContent = doc.ground_truth;
// Diffs
const panels = document.getElementById('doc-diff-panels');
panels.innerHTML = doc.engine_results.map((er, i) => {
const c = cerColor(er.cer); const bg = cerBg(er.cer);
const diffHtml = renderDiff(er.diff);
const errBadge = er.error ? `<span class="badge" style="background:#fee2e2;color:#dc2626">Erreur</span>` : '';
// Pipeline badge dans l'en-tête du panneau
const isPipeline = er.ocr_intermediate !== undefined;
const modeLabel = {text_only:'texte seul', text_and_image:'image+texte', zero_shot:'zero-shot'}[er.pipeline_mode] || '';
const pipeTagPanel = isPipeline
? `<span class="pipeline-tag">⛓ ${modeLabel || 'pipeline'}</span>` : '';
// Sur-normalisation (classe 10)
let onBadge = '';
if (er.over_normalization) {
const on = er.over_normalization;
const onPct = (on.score * 100).toFixed(2);
const cls = on.score > 0.05 ? 'over-norm-badge high' : 'over-norm-badge';
onBadge = `<span class="${cls}" title="Classe 10 — sur-normalisation LLM">Sur-norm. ${onPct}%</span>`;
}
// Triple-diff (vue spécifique pipeline) : OCR brut / Correction LLM
let tripleDiffHtml = '';
if (isPipeline && er.ocr_intermediate) {
const ocrDiffHtml = renderDiff(er.ocr_diff);
const llmDiffHtml = renderDiff(er.llm_correction_diff);
tripleDiffHtml = `
<div class="triple-diff-wrap">
<div class="triple-diff-section">
<h5>GT → OCR brut</h5>
${ocrDiffHtml || '<em style="color:var(--text-muted)"></em>'}
</div>
<div class="triple-diff-section">
<h5>OCR brut → Correction LLM</h5>
${llmDiffHtml || '<em style="color:var(--text-muted)"></em>'}
</div>
</div>`;
}
// CER diplomatique par document
let diplomaBadge = '';
if (er.cer_diplomatic !== null && er.cer_diplomatic !== undefined) {
const dipC = cerColor(er.cer_diplomatic); const dipB = cerBg(er.cer_diplomatic);
const delta = er.cer - er.cer_diplomatic;
const deltaHint = delta > 0.001 ? ` (−${(delta*100).toFixed(1)}% avec normalisation)` : '';
diplomaBadge = `<span class="cer-badge" style="color:${dipC};background:${dipB};opacity:.85"
title="CER diplomatique (ſ=s, u=v, i=j…)${deltaHint}">diplo. ${pct(er.cer_diplomatic)}</span>`;
}
return `<div class="diff-panel">
<div class="diff-panel-header">
<span class="diff-panel-title">${esc(er.engine)}</span>
${pipeTagPanel}
<span class="diff-panel-metrics">
<span class="cer-badge" style="color:${c};background:${bg}">${pct(er.cer)}</span>
${diplomaBadge}
<span class="badge" style="background:#f1f5f9">WER ${pct(er.wer)}</span>
${onBadge}
${errBadge}
</span>
</div>
<div class="diff-panel-body">${diffHtml || '<em style="color:var(--text-muted)">Aucune sortie</em>'}</div>
${tripleDiffHtml}
</div>`;
}).join('');
// ── Sprint 10 : distribution CER par ligne ──────────────────────────
const lineCard = document.getElementById('doc-line-metrics-card');
const lineContent = document.getElementById('doc-line-metrics-content');
// Prendre le premier moteur ayant des line_metrics
const erWithLine = doc.engine_results.find(er => er.line_metrics);
if (erWithLine && erWithLine.line_metrics) {
lineCard.style.display = '';
lineContent.innerHTML = renderLineMetrics(doc.engine_results);
} else {
lineCard.style.display = 'none';
}
// ── Sprint 10 : hallucinations ──────────────────────────────────────
const hallCard = document.getElementById('doc-hallucination-card');
const hallContent = document.getElementById('doc-hallucination-content');
const erWithHall = doc.engine_results.find(er => er.hallucination_metrics && er.hallucination_metrics.is_hallucinating);
if (erWithHall || doc.engine_results.some(er => er.hallucination_metrics)) {
hallCard.style.display = '';
hallContent.innerHTML = renderHallucinationPanel(doc.engine_results);
} else {
hallCard.style.display = 'none';
}
}
// ── Sprint 10 : rendu distribution CER par ligne ────────────────
function renderLineMetrics(engineResults) {
const heatmapColors = (v) => {
if (v < 0.05) return '#86efac';
if (v < 0.15) return '#fde68a';
if (v < 0.30) return '#fb923c';
return '#f87171';
};
return engineResults.filter(er => er.line_metrics).map(er => {
const lm = er.line_metrics;
const c = cerColor(er.cer); const bg = cerBg(er.cer);
// Heatmap de position
const heatmap = lm.heatmap || [];
const maxHeat = Math.max(...heatmap, 0.01);
const heatmapHtml = heatmap.length > 0
? `<div class="heatmap-wrap">` +
heatmap.map((v, i) => {
const h = Math.max(4, Math.round(60 * v / maxHeat));
return `<div class="heatmap-bar" style="height:${h}px;background:${heatmapColors(v)}"
title="Tranche ${i+1}/${heatmap.length} — CER=${(v*100).toFixed(1)}%"></div>`;
}).join('') +
`</div><div class="heatmap-labels"><span>${I18N.heatmap_start||'Début'}</span><span>${I18N.heatmap_mid||'Milieu'}</span><span>${I18N.heatmap_end||'Fin'}</span></div>`
: '<em style="color:var(--text-muted)"></em>';
// Percentiles
const p = lm.percentiles || {};
const pctBars = ['p50','p75','p90','p95','p99'].map(k => {
const v = p[k] || 0;
const w = Math.min(100, v * 100 * 2);
const fillColor = v < 0.15 ? '#86efac' : v < 0.30 ? '#fde68a' : '#f87171';
return `<div class="pct-bar-row">
<span class="pct-bar-label">${k}</span>
<div class="pct-bar-track"><div class="pct-bar-fill" style="width:${w}%;background:${fillColor}"></div></div>
<span class="pct-bar-val">${(v*100).toFixed(1)}%</span>
</div>`;
}).join('');
// Taux catastrophiques
const cr = lm.catastrophic_rate || {};
const crRows = Object.entries(cr).map(([t, rate]) => {
const tPct = (parseFloat(t)*100).toFixed(0);
const ratePct = (rate*100).toFixed(1);
const color = rate < 0.05 ? '#16a34a' : rate < 0.15 ? '#ca8a04' : '#dc2626';
return `<span class="stat"><b style="color:${color}">${ratePct}%</b> lignes CER&gt;${tPct}%</span>`;
}).join('');
// Gini
const gini = lm.gini !== undefined ? lm.gini.toFixed(3) : '—';
const giniColor = lm.gini < 0.3 ? '#16a34a' : lm.gini < 0.5 ? '#ca8a04' : '#dc2626';
return `<div style="margin-bottom:1.25rem;padding-bottom:1rem;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem">
<strong>${esc(er.engine)}</strong>
<span class="cer-badge" style="color:${c};background:${bg}">${pct(er.cer)}</span>
<span class="stat">Gini <b style="color:${giniColor}">${gini}</b></span>
<span class="stat">${lm.line_count} ${I18N.lines||'lignes'}</span>
${crRows}
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<div>
<div style="font-size:.75rem;font-weight:600;color:var(--text-muted);margin-bottom:.3rem">${I18N.heatmap_title||'CARTE THERMIQUE (position)'}</div>
${heatmapHtml}
</div>
<div>
<div style="font-size:.75rem;font-weight:600;color:var(--text-muted);margin-bottom:.3rem">${I18N.percentile_title||'PERCENTILES CER'}</div>
<div class="pct-bars">${pctBars}</div>
</div>
</div>
</div>`;
}).join('') || `<em style="color:var(--text-muted)">${I18N.no_line_metrics||'Aucune métrique de ligne disponible.'}</em>`;
}
// ── Sprint 10 : rendu panneau hallucinations ─────────────────────
function renderHallucinationPanel(engineResults) {
const withHall = engineResults.filter(er => er.hallucination_metrics);
if (!withHall.length) return `<em style="color:var(--text-muted)">${I18N.no_hall_metrics||"Aucune métrique d'hallucination disponible."}</em>`;
return withHall.map(er => {
const hm = er.hallucination_metrics;
const isHall = hm.is_hallucinating;
const badgeClass = isHall ? 'hallucination-badge' : 'hallucination-badge ok';
const badgeLabel = isHall ? (I18N.hall_detected||'⚠️ Hallucinations détectées') : (I18N.hall_ok||'✓ Ancrage satisfaisant');
const blocksHtml = hm.hallucinated_blocks && hm.hallucinated_blocks.length > 0
? hm.hallucinated_blocks.slice(0, 5).map(b =>
`<div class="halluc-block">
<div class="halluc-block-meta">${I18N.hall_block_label||'Bloc halluciné'} — ${b.length} mots (tokens ${b.start_token}–${b.end_token})</div>
${esc(b.text)}
</div>`
).join('') +
(hm.hallucinated_blocks.length > 5 ? `<div style="font-size:.72rem;color:var(--text-muted);margin-top:.25rem">… ${hm.hallucinated_blocks.length - 5} ${I18N.hall_more_blocks||'bloc(s) supplémentaire(s)'}</div>` : '')
: `<em style="color:var(--text-muted);font-size:.8rem">${I18N.no_hall_blocks||'Aucun bloc halluciné détecté.'}</em>`;
return `<div style="margin-bottom:1.25rem;padding-bottom:1rem;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;flex-wrap:wrap">
<strong>${esc(er.engine)}</strong>
<span class="${badgeClass}">${badgeLabel}</span>
<span class="stat">Ancrage <b>${(hm.anchor_score*100).toFixed(1)}%</b></span>
<span class="stat">Ratio longueur <b>${hm.length_ratio.toFixed(2)}</b></span>
<span class="stat">Insertion nette <b>${(hm.net_insertion_rate*100).toFixed(1)}%</b></span>
<span class="stat">${hm.gt_word_count} mots GT / ${hm.hyp_word_count} mots sortie</span>
</div>
${isHall ? `<div style="margin-bottom:.5rem;font-size:.82rem;font-weight:600;color:#9d174d">${I18N.hall_blocks_title||'Blocs sans ancrage dans le GT :'}</div>` : ''}
${isHall ? blocksHtml : ''}
</div>`;
}).join('');
}
// ── Sprint 10 — Scatter Gini vs CER moyen ──────────────────────
function buildGiniCerScatter() {
const canvas = document.getElementById('chart-gini-cer');
if (!canvas) return;
const pts = DATA.gini_vs_cer || [];
if (!pts.length) {
canvas.parentElement.innerHTML = `<p style="color:var(--text-muted);padding:1rem">${I18N.no_gini||'Données Gini non disponibles.'}</p>`;
return;
}
const datasets = pts.map((p, i) => ({
label: p.engine,
data: [{ x: p.cer * 100, y: p.gini }],
backgroundColor: engineColor(DATA.engines.findIndex(e => e.name === p.engine)) + 'cc',
borderColor: engineColor(DATA.engines.findIndex(e => e.name === p.engine)),
borderWidth: p.is_pipeline ? 2 : 1,
pointRadius: p.is_pipeline ? 9 : 7,
pointStyle: p.is_pipeline ? 'triangle' : 'circle',
}));
chartInstances['gini-cer'] = new Chart(canvas.getContext('2d'), {
type: 'scatter',
data: { datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { position: 'top', labels: { font: { size: 11 } } },
tooltip: { callbacks: {
label: ctx => `${ctx.dataset.label}: CER=${ctx.parsed.x.toFixed(2)}%, Gini=${ctx.parsed.y.toFixed(3)}`,
} },
},
scales: {
x: { min: 0, title: { display: true, text: 'CER moyen (%)', font: { size: 11 } } },
y: { min: 0, max: 1, title: { display: true, text: 'Coefficient de Gini', font: { size: 11 } } },
},
},
});
}
// ── Sprint 10 — Scatter ratio longueur vs score d'ancrage ────────
function buildRatioAnchorScatter() {
const canvas = document.getElementById('chart-ratio-anchor');
if (!canvas) return;
const pts = DATA.ratio_vs_anchor || [];
if (!pts.length) {
canvas.parentElement.innerHTML = '<p style="color:var(--text-muted);padding:1rem">Données d'ancrage non disponibles.</p>';
return;
}
// Zone de danger (ancrage < 0.5 OU ratio > 1.2) dessinée via plugin
const datasets = pts.map((p, i) => ({
label: p.engine + (p.is_vlm ? ' 👁' : ''),
data: [{ x: p.anchor_score, y: p.length_ratio }],
backgroundColor: engineColor(DATA.engines.findIndex(e => e.name === p.engine)) + 'cc',
borderColor: engineColor(DATA.engines.findIndex(e => e.name === p.engine)),
borderWidth: p.is_vlm ? 3 : 1,
pointRadius: p.is_vlm ? 10 : 7,
pointStyle: p.is_vlm ? 'star' : 'circle',
}));
chartInstances['ratio-anchor'] = new Chart(canvas.getContext('2d'), {
type: 'scatter',
data: { datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { position: 'top', labels: { font: { size: 11 } } },
tooltip: { callbacks: {
label: ctx => `${ctx.dataset.label}: ancrage=${(ctx.parsed.x*100).toFixed(1)}%, ratio=${ctx.parsed.y.toFixed(2)}`,
} },
},
scales: {
x: { min: 0, max: 1, title: { display: true, text: 'Score d'ancrage [0–1]', font: { size: 11 } } },
y: { min: 0, title: { display: true, text: 'Ratio longueur (sortie/GT)', font: { size: 11 } } },
},
},
plugins: [{
id: 'danger-zones',
beforeDraw(chart) {
const { ctx: c, chartArea: { left, top, right, bottom }, scales: { x, y } } = chart;
c.save();
// Ancrage < 0.5 (gauche)
const xHalf = x.getPixelForValue(0.5);
c.fillStyle = 'rgba(239,68,68,0.07)';
c.fillRect(left, top, xHalf - left, bottom - top);
// Ratio > 1.2 (haut)
const y12 = y.getPixelForValue(1.2);
if (y12 > top) {
c.fillRect(left, top, right - left, y12 - top);
}
// Lignes de seuil
c.strokeStyle = 'rgba(239,68,68,0.35)'; c.lineWidth = 1; c.setLineDash([4,4]);
c.beginPath(); c.moveTo(xHalf, top); c.lineTo(xHalf, bottom); c.stroke();
if (y12 > top) {
c.beginPath(); c.moveTo(left, y12); c.lineTo(right, y12); c.stroke();
}
c.restore();
},
}],
});
}
function buildDocList() {
const list = document.getElementById('doc-list');
list.innerHTML = DATA.documents.map(doc => {
const c = cerColor(doc.mean_cer); const bg = cerBg(doc.mean_cer);
return `<div class="doc-list-item" data-doc-id="${esc(doc.doc_id)}"
onclick="loadDocument('${esc(doc.doc_id)}')">
<span class="doc-list-label">${esc(doc.doc_id)}</span>
<span class="doc-list-cer" style="color:${c};background:${bg}">${pct(doc.mean_cer,1)}</span>
</div>`;
}).join('');
if (DATA.documents.length) loadDocument(DATA.documents[0].doc_id);
}
// Zoom
function handleZoom(e) {
e.preventDefault();
zoom(e.deltaY < 0 ? 1.15 : 0.87);
}
function zoom(factor) {
zoomLevel = Math.max(0.5, Math.min(5, zoomLevel * factor));
applyZoom();
}
function resetZoom() {
zoomLevel = 1; imgOffset = { x: 0, y: 0 };
applyZoom();
}
function applyZoom() {
const img = document.getElementById('doc-image');
img.style.transform = `scale(${zoomLevel}) translate(${imgOffset.x}px, ${imgOffset.y}px)`;
}
function startDrag(e) {
if (zoomLevel <= 1) return;
dragStart = { x: e.clientX - imgOffset.x * zoomLevel, y: e.clientY - imgOffset.y * zoomLevel };
document.getElementById('doc-image-wrap').style.cursor = 'grabbing';
}
function doDrag(e) {
if (!dragStart) return;
imgOffset.x = (e.clientX - dragStart.x) / zoomLevel;
imgOffset.y = (e.clientY - dragStart.y) / zoomLevel;
applyZoom();
}
function endDrag() {
dragStart = null;
document.getElementById('doc-image-wrap').style.cursor = zoomLevel > 1 ? 'grab' : 'zoom-in';
}
// ── Graphiques ──────────────────────────────────────────────────
let chartsBuilt = false;
let chartInstances = {};
function destroyChart(id) {
if (chartInstances[id]) { chartInstances[id].destroy(); delete chartInstances[id]; }
}
function buildCharts() {
if (chartsBuilt) return;
chartsBuilt = true;
buildCerHistogram();
buildRadar();
buildCerPerDoc();
buildDurationChart();
buildQualityCerScatter();
buildTaxonomyChart();
// Sprint 7
buildReliabilityCurves();
buildBootstrapCIChart();
buildVennDiagram();
buildWilcoxonTable();
buildErrorClusters();
initCorrelationMatrix();
// Sprint 10
buildGiniCerScatter();
buildRatioAnchorScatter();
}
function buildCerHistogram() {
destroyChart('cer-hist');
const ctx = document.getElementById('chart-cer-hist').getContext('2d');
// Construire histogramme à bins fixes [0-5, 5-10, 10-20, 20-30, 30-50, 50+]
const bins = [0, 0.05, 0.10, 0.20, 0.30, 0.50, 1.01];
const labels = ['0–5%', '5–10%', '10–20%', '20–30%', '30–50%', '>50%'];
const colors = ['#16a34a','#65a30d','#ca8a04','#ea580c','#dc2626','#9f1239'];
const datasets = DATA.engines.map((e, ei) => {
const counts = new Array(labels.length).fill(0);
e.cer_values.forEach(v => {
for (let i = 0; i < bins.length - 1; i++) {
if (v >= bins[i] && v < bins[i+1]) { counts[i]++; break; }
}
});
return {
label: e.name, data: counts,
backgroundColor: engineColor(ei) + 'aa',
borderColor: engineColor(ei),
borderWidth: 1,
};
});
chartInstances['cer-hist'] = new Chart(ctx, {
type: 'bar',
data: { labels, datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { position: 'top', labels: { font: { size: 11 } } } },
scales: {
x: { title: { display: true, text: 'Plage CER', font: { size: 11 } } },
y: { title: { display: true, text: 'Nombre de documents', font: { size: 11 } },
ticks: { stepSize: 1 } },
},
},
});
}
function buildRadar() {
destroyChart('radar');
const ctx = document.getElementById('chart-radar').getContext('2d');
// Axes : CER, WER, MER, WIL inversés (1 - valeur → plus c'est élevé, mieux c'est)
const metrics = ['CER', 'WER', 'MER', 'WIL'];
const keys = ['cer', 'wer', 'mer', 'wil'];
const datasets = DATA.engines.map((e, i) => {
const data = keys.map(k => Math.max(0, (1 - (e[k] || 0)) * 100));
return {
label: e.name, data,
backgroundColor: engineColor(i) + '33',
borderColor: engineColor(i),
borderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
};
});
chartInstances['radar'] = new Chart(ctx, {
type: 'radar',
data: { labels: metrics, datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { position: 'top', labels: { font: { size: 11 } } } },
scales: {
r: {
min: 0, max: 100,
ticks: { stepSize: 20, font: { size: 10 } },
pointLabels: { font: { size: 12, weight: 'bold' } },
},
},
},
});
}
function buildCerPerDoc() {
destroyChart('cer-doc');
const ctx = document.getElementById('chart-cer-doc').getContext('2d');
const labels = DATA.documents.map(d => d.doc_id);
const datasets = DATA.engines.map((e, ei) => {
const data = DATA.documents.map(doc => {
const er = doc.engine_results.find(r => r.engine === e.name);
return er ? er.cer * 100 : null;
});
return {
label: e.name, data,
borderColor: engineColor(ei),
backgroundColor: engineColor(ei) + '22',
tension: 0.3, fill: false,
pointRadius: 3, pointHoverRadius: 5,
};
});
chartInstances['cer-doc'] = new Chart(ctx, {
type: 'line',
data: { labels, datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { position: 'top', labels: { font: { size: 11 } } } },
scales: {
x: { ticks: { maxRotation: 45, font: { size: 10 } } },
y: { title: { display: true, text: 'CER (%)', font: { size: 11 } }, min: 0 },
},
},
});
}
function buildDurationChart() {
destroyChart('duration');
const ctx = document.getElementById('chart-duration').getContext('2d');
const labels = DATA.engines.map(e => e.name);
const data = DATA.engines.map(e => {
const docs = DATA.documents;
const durs = docs.flatMap(d => d.engine_results
.filter(r => r.engine === e.name)
.map(r => r.duration));
const mean = durs.length ? durs.reduce((a,b) => a+b, 0) / durs.length : 0;
return parseFloat(mean.toFixed(3));
});
chartInstances['duration'] = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{
label: 'Durée moy. (s)',
data,
backgroundColor: DATA.engines.map((_, i) => engineColor(i) + 'aa'),
borderColor: DATA.engines.map((_, i) => engineColor(i)),
borderWidth: 1,
}],
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { title: { display: true, text: 'Secondes', font: { size: 11 } }, min: 0 },
},
},
});
}
function buildQualityCerScatter() {
const ctx = document.getElementById('chart-quality-cer');
if (!ctx) return;
// Construire les points : un par document, un dataset par moteur
const datasets = DATA.engines.map((e, ei) => {
const points = DATA.documents.flatMap(doc => {
const er = doc.engine_results.find(r => r.engine === e.name);
if (!er || er.error || !er.image_quality) return [];
return [{ x: er.image_quality.quality_score, y: er.cer * 100 }];
});
return {
label: e.name, data: points,
backgroundColor: engineColor(ei) + 'bb',
borderColor: engineColor(ei),
borderWidth: 1, pointRadius: 5, pointHoverRadius: 7,
};
}).filter(d => d.data.length > 0);
if (!datasets.length) { ctx.parentElement.innerHTML = '<p style="color:var(--text-muted);padding:1rem">Aucune donnée de qualité image disponible.</p>'; return; }
chartInstances['quality-cer'] = new Chart(ctx.getContext('2d'), {
type: 'scatter',
data: { datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { position: 'top', labels: { font: { size: 11 } } },
tooltip: { callbacks: {
label: ctx => `${ctx.dataset.label}: qualité=${ctx.parsed.x.toFixed(2)}, CER=${ctx.parsed.y.toFixed(1)}%`,
} },
},
scales: {
x: { min: 0, max: 1, title: { display: true, text: 'Score qualité image [0–1]', font: { size: 11 } } },
y: { min: 0, title: { display: true, text: 'CER (%)', font: { size: 11 } } },
},
},
});
}
function buildTaxonomyChart() {
const ctx = document.getElementById('chart-taxonomy');
if (!ctx) return;
const taxLabels = ['Confusion visuelle','Diacritique','Casse','Ligature','Abréviation','Hapax','Segmentation','Hors-vocab.','Lacune'];
const taxKeys = ['visual_confusion','diacritic_error','case_error','ligature_error','abbreviation_error','hapax','segmentation_error','oov_character','lacuna'];
const taxColors = ['#6366f1','#f59e0b','#ec4899','#14b8a6','#8b5cf6','#64748b','#f97316','#06b6d4','#ef4444'];
const datasets = DATA.engines.map((e, ei) => {
const tax = e.aggregated_taxonomy;
const data = taxKeys.map(k => tax && tax.counts ? (tax.counts[k] || 0) : 0);
return {
label: e.name, data,
backgroundColor: engineColor(ei) + '99',
borderColor: engineColor(ei),
borderWidth: 1,
};
});
chartInstances['taxonomy'] = new Chart(ctx.getContext('2d'), {
type: 'bar',
data: { labels: taxLabels, datasets },
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { position: 'top', labels: { font: { size: 11 } } } },
scales: {
x: { ticks: { font: { size: 10 } } },
y: { title: { display: true, text: "Nb d'erreurs", font: { size: 11 } }, min: 0, ticks: { stepSize: 1 } },
},
},
});
}
// ── Sprint 7 — Courbes de fiabilité ─────────────────────────────
function buildReliabilityCurves() {
const ctx = document.getElementById('chart-reliability');
if (!ctx) return;
const curves = DATA.reliability_curves || [];
if (!curves.length) { ctx.parentElement.innerHTML = '<p style="color:var(--text-muted);padding:1rem">Données insuffisantes.</p>'; return; }
const datasets = curves.map((c, i) => {
const points = (c.points || []).map(p => ({ x: p.pct_docs, y: p.mean_cer * 100 }));
return {
label: c.engine, data: points,
borderColor: engineColor(i), backgroundColor: engineColor(i) + '22',
tension: 0.3, fill: false, pointRadius: 2, pointHoverRadius: 5,
};
});
destroyChart('reliability');
chartInstances['reliability'] = new Chart(ctx.getContext('2d'), {
type: 'line',
data: { datasets },
options: {
responsive: true, maintainAspectRatio: false,
parsing: { xAxisKey: 'x', yAxisKey: 'y' },
plugins: {
legend: { position: 'top', labels: { font: { size: 11 } } },
tooltip: { callbacks: {
title: ([item]) => `${item.parsed.x.toFixed(0)}% docs les plus faciles`,
label: item => `${item.dataset.label}: CER moy = ${item.parsed.y.toFixed(2)}%`,
} },
},
scales: {
x: { type:'linear', min:0, max:100,
title: { display:true, text:'% documents (triés par CER croissant)', font:{ size:11 } } },
y: { min:0, title: { display:true, text:'CER moyen (%)', font:{ size:11 } } },
},
},
});
}
// ── Sprint 7 — Bootstrap CI ──────────────────────────────────────
function buildBootstrapCIChart() {
const ctx = document.getElementById('chart-bootstrap-ci');
if (!ctx) return;
const cis = DATA.statistics && DATA.statistics.bootstrap_cis || [];
if (!cis.length) { ctx.parentElement.innerHTML = '<p style="color:var(--text-muted);padding:1rem">Données insuffisantes.</p>'; return; }
const labels = cis.map(c => c.engine);
const means = cis.map(c => (c.mean * 100));
const lowers = cis.map(c => (c.mean - c.ci_lower) * 100);
const uppers = cis.map(c => (c.ci_upper - c.mean) * 100);
destroyChart('bootstrap-ci');
chartInstances['bootstrap-ci'] = new Chart(ctx.getContext('2d'), {
type: 'bar',
data: {
labels,
datasets: [{
label: 'CER moyen (%)',
data: means,
backgroundColor: cis.map((_, i) => engineColor(i) + 'aa'),
borderColor: cis.map((_, i) => engineColor(i)),
borderWidth: 1,
errorBars: { symmetric: false },
}],
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
afterLabel: (ctx) => {
const ci = cis[ctx.dataIndex];
return `IC 95% : [${(ci.ci_lower*100).toFixed(2)}%, ${(ci.ci_upper*100).toFixed(2)}%]`;
},
},
},
},
scales: { y: { min: 0, title: { display:true, text:'CER (%)', font:{size:11} } } },
},
plugins: [{
id: 'errorBars',
afterDatasetsDraw(chart) {
const { ctx: c, data, scales: { x, y } } = chart;
chart.data.datasets[0].data.forEach((val, i) => {
const ci = cis[i];
if (!ci) return;
const xPos = x.getPixelForValue(i);
const yTop = y.getPixelForValue(ci.ci_upper * 100);
const yBot = y.getPixelForValue(ci.ci_lower * 100);
c.save();
c.strokeStyle = '#374151'; c.lineWidth = 2;
c.beginPath(); c.moveTo(xPos, yTop); c.lineTo(xPos, yBot); c.stroke();
c.beginPath(); c.moveTo(xPos-6, yTop); c.lineTo(xPos+6, yTop); c.stroke();
c.beginPath(); c.moveTo(xPos-6, yBot); c.lineTo(xPos+6, yBot); c.stroke();
c.restore();
});
},
}],
});
}
// ── Sprint 7 — Diagramme de Venn ────────────────────────────────
function buildVennDiagram() {
const container = document.getElementById('venn-container');
if (!container) return;
const venn = DATA.venn_data;
if (!venn || !venn.type) {
container.innerHTML = '<p style="color:var(--text-muted)">Données insuffisantes pour le diagramme de Venn.</p>';
return;
}
if (venn.type === 'venn2') {
const total = (venn.only_a || 0) + (venn.both || 0) + (venn.only_b || 0);
const maxR = 80;
const rA = Math.sqrt((venn.only_a + venn.both) / (total || 1)) * maxR + 30;
const rB = Math.sqrt((venn.only_b + venn.both) / (total || 1)) * maxR + 30;
const overlap = venn.both > 0 ? Math.min(rA, rB) * 0.6 : 0;
const cxA = 140, cxB = cxA + rA + rB - overlap, cy = 130;
const w = cxB + rB + 20, h = 260;
container.innerHTML = `
<div style="text-align:center">
<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" style="max-width:100%">
<circle cx="${cxA}" cy="${cy}" r="${rA}" fill="#2563eb" fill-opacity="0.25" stroke="#2563eb" stroke-width="2"/>
<circle cx="${cxB}" cy="${cy}" r="${rB}" fill="#dc2626" fill-opacity="0.25" stroke="#dc2626" stroke-width="2"/>
<text x="${cxA - rA*0.5}" y="${cy}" text-anchor="middle" font-size="13" font-weight="bold" fill="#1e40af">${venn.only_a}</text>
<text x="${(cxA + cxB)/2}" y="${cy}" text-anchor="middle" font-size="13" font-weight="bold" fill="#374151">${venn.both}</text>
<text x="${cxB + rB*0.5}" y="${cy}" text-anchor="middle" font-size="13" font-weight="bold" fill="#b91c1c">${venn.only_b}</text>
<text x="${cxA - rA*0.5}" y="${cy + rA + 14}" text-anchor="middle" font-size="11" fill="#2563eb">${esc(venn.label_a)}</text>
<text x="${cxB + rB*0.5}" y="${cy + rB + 14}" text-anchor="middle" font-size="11" fill="#dc2626">${esc(venn.label_b)}</text>
<text x="${(cxA+cxB)/2}" y="${cy + Math.min(rA,rB) + 14}" text-anchor="middle" font-size="10" fill="#64748b">commun</text>
</svg>
<p style="font-size:.75rem;color:var(--text-muted);margin-top:.25rem">
Erreurs exclusives ${esc(venn.label_a)} : ${venn.only_a} ·
Communes : ${venn.both} ·
Exclusives ${esc(venn.label_b)} : ${venn.only_b}
</p>
</div>
`;
} else if (venn.type === 'venn3') {
// Venn 3 cercles simplifié
const total = (venn.only_a||0)+(venn.only_b||0)+(venn.only_c||0)+(venn.ab||0)+(venn.ac||0)+(venn.bc||0)+(venn.abc||0) || 1;
container.innerHTML = `
<div style="text-align:center">
<svg width="300" height="280" viewBox="0 0 300 280" style="max-width:100%">
<circle cx="130" cy="110" r="80" fill="#2563eb" fill-opacity="0.2" stroke="#2563eb" stroke-width="1.5"/>
<circle cx="170" cy="110" r="80" fill="#dc2626" fill-opacity="0.2" stroke="#dc2626" stroke-width="1.5"/>
<circle cx="150" cy="155" r="80" fill="#16a34a" fill-opacity="0.2" stroke="#16a34a" stroke-width="1.5"/>
<text x="95" y="95" text-anchor="middle" font-size="12" font-weight="bold" fill="#1e40af">${venn.only_a}</text>
<text x="205" y="95" text-anchor="middle" font-size="12" font-weight="bold" fill="#b91c1c">${venn.only_b}</text>
<text x="150" y="230" text-anchor="middle" font-size="12" font-weight="bold" fill="#15803d">${venn.only_c}</text>
<text x="148" y="108" text-anchor="middle" font-size="11" fill="#374151">${venn.ab}</text>
<text x="120" y="160" text-anchor="middle" font-size="11" fill="#374151">${venn.ac}</text>
<text x="180" y="160" text-anchor="middle" font-size="11" fill="#374151">${venn.bc}</text>
<text x="150" y="145" text-anchor="middle" font-size="11" font-weight="bold" fill="#374151">${venn.abc}</text>
<text x="95" y="127" text-anchor="middle" font-size="9" fill="#2563eb">${esc((venn.label_a||'').slice(0,10))}</text>
<text x="205" y="127" text-anchor="middle" font-size="9" fill="#dc2626">${esc((venn.label_b||'').slice(0,10))}</text>
<text x="150" y="248" text-anchor="middle" font-size="9" fill="#16a34a">${esc((venn.label_c||'').slice(0,10))}</text>
</svg>
</div>
`;
}
}
// ── Sprint 7 — Table de Wilcoxon ─────────────────────────────────
function buildWilcoxonTable() {
const container = document.getElementById('wilcoxon-table-container');
if (!container) return;
const stats = DATA.statistics && DATA.statistics.pairwise_wilcoxon || [];
if (!stats.length) {
container.innerHTML = '<p style="color:var(--text-muted)">Pas assez de données pour les tests statistiques (min 2 concurrents).</p>';
return;
}
const rows = stats.map(s => {
const sigClass = s.significant ? 'stat-sig' : 'stat-ns';
const sigLabel = s.significant ? '✓ Significative' : '○ Non significative';
return `<tr>
<td style="padding:.4rem .6rem;font-weight:600">${esc(s.engine_a)}</td>
<td style="padding:.4rem .3rem;color:var(--text-muted)">vs</td>
<td style="padding:.4rem .6rem;font-weight:600">${esc(s.engine_b)}</td>
<td style="padding:.4rem .6rem;text-align:right;font-variant-numeric:tabular-nums">${s.n_pairs}</td>
<td style="padding:.4rem .6rem;text-align:right;font-variant-numeric:tabular-nums">${s.statistic}</td>
<td style="padding:.4rem .6rem;text-align:right;font-variant-numeric:tabular-nums">${s.p_value}</td>
<td style="padding:.4rem .75rem"><span class="${sigClass}">${sigLabel}</span></td>
<td style="padding:.4rem .75rem;font-size:.78rem;color:var(--text-muted);max-width:280px">${esc(s.interpretation)}</td>
</tr>`;
}).join('');
container.innerHTML = `
<table style="border-collapse:collapse;font-size:.84rem;width:100%">
<thead><tr style="background:var(--bg)">
<th style="padding:.4rem .6rem;text-align:left;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em">Concurrent A</th>
<th></th>
<th style="padding:.4rem .6rem;text-align:left;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em">Concurrent B</th>
<th style="padding:.4rem .6rem;text-align:right;font-size:.75rem">N paires</th>
<th style="padding:.4rem .6rem;text-align:right;font-size:.75rem">W</th>
<th style="padding:.4rem .6rem;text-align:right;font-size:.75rem">p-value</th>
<th style="padding:.4rem .75rem;text-align:left;font-size:.75rem">Verdict</th>
<th style="padding:.4rem .75rem;text-align:left;font-size:.75rem">Interprétation</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
`;
}
// ── Sprint 7 — Clustering des erreurs ───────────────────────────
function buildErrorClusters() {
const container = document.getElementById('error-clusters-container');
if (!container) return;
const clusters = DATA.error_clusters || [];
if (!clusters.length) {
container.innerHTML = '<p style="color:var(--text-muted)">Aucun cluster d'erreur détecté.</p>';
return;
}
const cards = clusters.map(cl => {
const examplesHtml = (cl.examples || []).slice(0, 3).map(ex => {
const oldStr = ex.gt_fragment || '';
const newStr = ex.ocr_fragment || '';
return `<div class="cluster-ex">
<span class="ex-old">${esc(oldStr || '∅')}</span>
<span style="color:var(--text-muted)"></span>
<span class="ex-new">${esc(newStr || '∅')}</span>
<span style="color:var(--text-muted);font-size:.72rem">(${esc(ex.engine || '')})</span>
</div>`;
}).join('');
return `<div class="cluster-card">
<div class="cluster-label">Cluster #${cl.cluster_id} : ${esc(cl.label)}</div>
<div class="cluster-count">${cl.count} cas détectés</div>
<div class="cluster-examples">${examplesHtml}</div>
</div>`;
}).join('');
container.innerHTML = `<div class="cluster-grid">${cards}</div>`;
}
// ── Sprint 7 — Matrice de corrélation ───────────────────────────
function initCorrelationMatrix() {
const sel = document.getElementById('corr-engine-select');
if (!sel) return;
const corrs = DATA.correlation_per_engine || [];
sel.innerHTML = '';
corrs.forEach(c => {
const opt = document.createElement('option');
opt.value = c.engine; opt.textContent = c.engine;
sel.appendChild(opt);
});
renderCorrelationMatrix();
}
function renderCorrelationMatrix() {
const container = document.getElementById('corr-matrix-container');
if (!container) return;
const sel = document.getElementById('corr-engine-select');
const engineName = sel && sel.value;
const corrs = DATA.correlation_per_engine || [];
const entry = corrs.find(c => c.engine === engineName) || corrs[0];
if (!entry || !entry.labels || !entry.matrix) {
container.innerHTML = '<p style="color:var(--text-muted)">Données insuffisantes.</p>';
return;
}
const labels = entry.labels;
const matrix = entry.matrix;
const n = labels.length;
const labelNames = {
cer: 'CER', wer: 'WER', mer: 'MER', wil: 'WIL',
quality_score: 'Qualité img', sharpness: 'Netteté',
ligature: 'Ligatures', diacritic: 'Diacritiques',
};
function corrColor(r) {
if (r >= 0.7) return 'background:#dcfce7;color:#14532d';
if (r >= 0.3) return 'background:#f0fdf4;color:#166534';
if (r >= -0.3) return 'background:#f8fafc;color:#374151';
if (r >= -0.7) return 'background:#fef2f2;color:#991b1b';
return 'background:#fee2e2;color:#7f1d1d';
}
const headerRow = '<tr><th></th>' + labels.map(l =>
`<th>${esc(labelNames[l] || l)}</th>`).join('') + '</tr>';
const dataRows = matrix.map((row, i) =>
'<tr><th style="text-align:right">' + esc(labelNames[labels[i]] || labels[i]) + '</th>' +
row.map((v, j) => {
const style = corrColor(v);
const display = i === j ? '1.00' : v.toFixed(2);
return `<td style="${style}">${display}</td>`;
}).join('') + '</tr>'
).join('');
container.innerHTML = `<table class="corr-table"><thead>${headerRow}</thead><tbody>${dataRows}</tbody></table>`;
}
// ── Sprint 7 — URL stateful ──────────────────────────────────────
function updateURL(view, params) {
const hash = '#' + view + (params ? '?' + new URLSearchParams(params).toString() : '');
history.replaceState(null, '', hash);
}
function readURLState() {
const hash = location.hash.slice(1);
const [view, query] = hash.split('?');
const params = query ? Object.fromEntries(new URLSearchParams(query)) : {};
return { view: view || 'ranking', params };
}
// ── Sprint 7 — Mode présentation ────────────────────────────────
let presentMode = false;
function togglePresentMode() {
presentMode = !presentMode;
document.body.classList.toggle('present-mode', presentMode);
const btn = document.getElementById('btn-present');
if (btn) {
btn.classList.toggle('active', presentMode);
btn.textContent = presentMode ? '⊡ Normal' : '⊞ Présentation';
}
}
// ── Sprint 7 — Export CSV ────────────────────────────────────────
function exportCSV() {
const rows = [['doc_id','engine','cer','wer','mer','wil','duration','ligature_score','diacritic_score','difficulty_score','gini','anchor_score','length_ratio','is_hallucinating']];
DATA.documents.forEach(doc => {
doc.engine_results.forEach(er => {
rows.push([
doc.doc_id,
er.engine,
er.cer !== null ? (er.cer * 100).toFixed(4) : '',
er.wer !== null ? (er.wer * 100).toFixed(4) : '',
er.mer !== null ? (er.mer * 100).toFixed(4) : '',
er.wil !== null ? (er.wil * 100).toFixed(4) : '',
er.duration !== null ? er.duration : '',
er.ligature_score !== null ? er.ligature_score : '',
er.diacritic_score !== null ? er.diacritic_score : '',
doc.difficulty_score !== undefined ? (doc.difficulty_score * 100).toFixed(2) : '',
er.line_metrics ? er.line_metrics.gini.toFixed(6) : '',
er.hallucination_metrics ? er.hallucination_metrics.anchor_score.toFixed(6) : '',
er.hallucination_metrics ? er.hallucination_metrics.length_ratio.toFixed(4) : '',
er.hallucination_metrics ? (er.hallucination_metrics.is_hallucinating ? '1' : '0') : '',
]);
});
});
const csv = rows.map(r => r.map(v => JSON.stringify(String(v ?? ''))).join(',')).join('
');
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'picarones_metrics_' + DATA.meta.corpus_name.replace(/\s+/g,'-') + '.csv';
document.body.appendChild(a); a.click();
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
}
// ── Vue Caractères ───────────────────────────────────────────────
let charViewBuilt = false;
function initCharView() {
charViewBuilt = true;
// Remplir le sélecteur de moteur
const sel = document.getElementById('char-engine-select');
sel.innerHTML = '';
DATA.engines.forEach(e => {
const opt = document.createElement('option');
opt.value = e.name; opt.textContent = e.name;
sel.appendChild(opt);
});
renderCharView();
}
function renderCharView() {
const engineName = document.getElementById('char-engine-select').value;
const eng = DATA.engines.find(e => e.name === engineName);
if (!eng) return;
// Scores ligatures / diacritiques
const scoresRow = document.getElementById('char-scores-row');
const ligScore = eng.ligature_score;
const diacScore = eng.diacritic_score;
scoresRow.innerHTML = `
<div class="stat">Ligatures <b>${_scoreBadge(ligScore, 'Ligatures')}</b></div>
<div class="stat">Diacritiques <b>${_scoreBadge(diacScore, 'Diacritiques')}</b></div>
${eng.aggregated_structure ? `
<div class="stat">Précision lignes <b>${_scoreBadge(eng.aggregated_structure.mean_line_accuracy, 'Précision nb lignes')}</b></div>
<div class="stat">Ordre lecture <b>${_scoreBadge(eng.aggregated_structure.mean_reading_order_score, 'Score ordre de lecture')}</b></div>
` : ''}
${eng.aggregated_image_quality ? `
<div class="stat">Qualité image moy. <b>${_scoreBadge(eng.aggregated_image_quality.mean_quality_score, 'Qualité image moyenne')}</b></div>
` : ''}
`;
// Matrice de confusion heatmap
renderConfusionHeatmap(eng);
// Détail ligatures
renderLigatureDetail(eng);
// Taxonomie détaillée
renderTaxonomyDetail(eng);
}
function renderConfusionHeatmap(eng) {
const container = document.getElementById('confusion-heatmap');
const cm = eng.aggregated_confusion;
if (!cm || !cm.matrix) {
container.innerHTML = '<p style="color:var(--text-muted)">Aucune donnée de confusion disponible.</p>';
return;
}
// Collecter les top confusions (substitutions uniquement, hors ∅)
const pairs = [];
for (const [gt, ocrs] of Object.entries(cm.matrix)) {
if (gt === '∅') continue;
for (const [ocr, cnt] of Object.entries(ocrs)) {
if (ocr !== gt && ocr !== '∅' && cnt > 0) {
pairs.push({ gt, ocr, cnt });
}
}
}
pairs.sort((a,b) => b.cnt - a.cnt);
const top = pairs.slice(0, 30);
if (!top.length) {
container.innerHTML = '<p style="color:var(--text-muted)">Aucune substitution détectée.</p>';
return;
}
// Heatmap sous forme de tableau compact
const maxCnt = top[0].cnt;
const rows = top.map(p => {
const intensity = Math.round((p.cnt / maxCnt) * 200 + 55); // 55–255
const bg = `rgb(${intensity},50,50)`;
const fg = intensity > 150 ? '#fff' : '#222';
return `<tr onclick="showConfusionExamples('${esc(p.gt)}','${esc(p.ocr)}')" style="cursor:pointer" title="GT='${esc(p.gt)}' → OCR='${esc(p.ocr)}' : ${p.cnt} fois">
<td style="font-family:monospace;font-size:1.1rem;padding:.3rem .6rem;text-align:center">${esc(p.gt)}</td>
<td style="padding:.1rem .3rem;color:var(--text-muted)"></td>
<td style="font-family:monospace;font-size:1.1rem;padding:.3rem .6rem;text-align:center">${esc(p.ocr)}</td>
<td style="padding:.3rem 1rem">
<div style="display:flex;align-items:center;gap:.5rem">
<div style="width:${Math.round(p.cnt/maxCnt*120)}px;height:12px;border-radius:3px;background:${bg}"></div>
<span style="font-size:.8rem;color:var(--text-muted)">${p.cnt}×</span>
</div>
</td>
</tr>`;
}).join('');
container.innerHTML = `
<p style="font-size:.75rem;color:var(--text-muted);margin-bottom:.5rem">
Cliquer sur une ligne pour voir les exemples dans la vue Document.
Total substitutions : <b>${cm.total_substitutions}</b>
· Insertions : <b>${cm.total_insertions}</b>
· Suppressions : <b>${cm.total_deletions}</b>
</p>
<table style="border-collapse:collapse;font-size:.85rem">
<thead><tr>
<th style="padding:.3rem .6rem;text-align:left">GT</th>
<th></th>
<th style="padding:.3rem .6rem;text-align:left">OCR</th>
<th style="padding:.3rem 1rem;text-align:left">Fréquence</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
`;
}
function showConfusionExamples(gtChar, ocrChar) {
// Naviguer vers la vue Document en cherchant un exemple de cette confusion
showView('document');
const docWithConfusion = DATA.documents.find(doc =>
doc.engine_results.some(er => {
const h = er.hypothesis || '';
const g = doc.ground_truth || '';
return g.includes(gtChar) && h.includes(ocrChar);
})
);
if (docWithConfusion) loadDocument(docWithConfusion.doc_id);
}
function renderLigatureDetail(eng) {
const container = document.getElementById('ligature-detail');
// Agrégation sur tous les documents pour ce moteur
const ligData = {};
DATA.documents.forEach(doc => {
const er = doc.engine_results.find(r => r.engine === eng.name);
if (!er || !er.ligature_score) return;
// On n'a que le score global par doc; pour le détail, utiliser aggregated_char_scores
});
const agg = eng.aggregated_char_scores;
if (!agg || !agg.ligature || !agg.ligature.per_ligature) {
const overallScore = eng.ligature_score;
if (overallScore !== null && overallScore !== undefined) {
container.innerHTML = `<div class="stat">Score global ligatures : ${_scoreBadge(overallScore, 'Ligatures')}</div>`;
} else {
container.innerHTML = '<p style="color:var(--text-muted)">Aucune donnée ligature disponible (pas de ligatures dans le corpus).</p>';
}
return;
}
const perLig = agg.ligature.per_ligature;
if (!Object.keys(perLig).length) {
container.innerHTML = '<p style="color:var(--text-muted)">Aucune ligature trouvée dans le corpus GT.</p>';
return;
}
const rows = Object.entries(perLig)
.sort((a,b) => b[1].gt_count - a[1].gt_count)
.map(([lig, d]) => {
const sc = d.score;
const color = sc >= 0.9 ? '#16a34a' : sc >= 0.7 ? '#ca8a04' : '#dc2626';
const barW = Math.round(sc * 120);
return `<tr>
<td style="font-family:monospace;font-size:1.2rem;padding:.3rem .6rem">${esc(lig)}</td>
<td style="padding:.3rem .6rem;font-size:.8rem;color:var(--text-muted)">${esc(lig.codePointAt(0).toString(16).toUpperCase().padStart(4,'0'))}</td>
<td style="padding:.3rem .6rem">${d.gt_count} GT</td>
<td style="padding:.3rem .6rem">${d.ocr_correct} corrects</td>
<td style="padding:.3rem 1rem">
<div style="display:flex;align-items:center;gap:.5rem">
<div style="width:${barW}px;height:10px;border-radius:3px;background:${color}"></div>
<span style="color:${color};font-weight:600">${(sc*100).toFixed(0)}%</span>
</div>
</td>
</tr>`;
}).join('');
container.innerHTML = `
<table style="border-collapse:collapse;font-size:.85rem">
<thead><tr>
<th style="padding:.3rem .6rem;text-align:left">Ligature</th>
<th style="padding:.3rem .6rem;text-align:left">Unicode</th>
<th style="padding:.3rem .6rem">GT</th>
<th style="padding:.3rem .6rem">Corrects</th>
<th style="padding:.3rem 1rem;text-align:left">Score</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
`;
}
function renderTaxonomyDetail(eng) {
const container = document.getElementById('taxonomy-detail');
const tax = eng.aggregated_taxonomy;
if (!tax || !tax.counts) {
container.innerHTML = '<p style="color:var(--text-muted)">Aucune donnée taxonomique disponible.</p>';
return;
}
const classNames = {
visual_confusion: '1 — Confusion visuelle',
diacritic_error: '2 — Erreur diacritique',
case_error: '3 — Erreur de casse',
ligature_error: '4 — Ligature',
abbreviation_error: '5 — Abréviation',
hapax: '6 — Hapax',
segmentation_error: '7 — Segmentation',
oov_character: '8 — Hors-vocabulaire',
lacuna: '9 — Lacune',
};
const total = tax.total_errors || 1;
const maxCnt = Math.max(...Object.values(tax.counts));
const rows = Object.entries(tax.counts)
.filter(([, cnt]) => cnt > 0)
.sort((a,b) => b[1]-a[1])
.map(([cls, cnt]) => {
const pctVal = (cnt / total * 100).toFixed(1);
const barW = maxCnt > 0 ? Math.round(cnt/maxCnt * 200) : 0;
return `<tr>
<td style="padding:.3rem .6rem;font-size:.85rem">${esc(classNames[cls] || cls)}</td>
<td style="padding:.3rem .6rem;text-align:right;font-variant-numeric:tabular-nums">${cnt}</td>
<td style="padding:.3rem 1rem">
<div style="display:flex;align-items:center;gap:.5rem">
<div style="width:${barW}px;height:10px;border-radius:3px;background:#6366f1"></div>
<span style="color:var(--text-muted);font-size:.8rem">${pctVal}%</span>
</div>
</td>
</tr>`;
}).join('');
container.innerHTML = `
<p style="font-size:.75rem;color:var(--text-muted);margin-bottom:.5rem">Total : <b>${tax.total_errors}</b> erreurs classifiées.</p>
<table style="border-collapse:collapse;font-size:.85rem;min-width:400px">
<thead><tr>
<th style="padding:.3rem .6rem;text-align:left">Classe</th>
<th style="padding:.3rem .6rem;text-align:right">N</th>
<th style="padding:.3rem 1rem;text-align:left">Proportion</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
`;
}
// ── Init ────────────────────────────────────────────────────────
// Override showView pour mettre à jour l'URL
const _origShowView = showView;
function showView(name) {
_origShowView(name);
updateURL(name);
}
function applyI18n() {
// Applique les traductions aux éléments avec data-i18n (textContent)
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (I18N[key] !== undefined) el.textContent = I18N[key];
});
// Options de select avec data-i18n-opt
document.querySelectorAll('[data-i18n-opt]').forEach(el => {
const key = el.getAttribute('data-i18n-opt');
if (I18N[key] !== undefined) el.textContent = I18N[key];
});
// Tooltips des th via id
const thMap = {
'th-cer-diplo': 'col_cer_diplo_title',
'th-ligatures': 'col_ligatures_title',
'th-diacritics': 'col_diacritics_title',
'th-gini': 'col_gini_title',
'th-anchor': 'col_anchor_title',
'th-overnorm': 'col_overnorm_title',
};
Object.entries(thMap).forEach(([id, key]) => {
const el = document.getElementById(id);
if (el && I18N[key]) el.title = I18N[key];
});
}
function init() {
// i18n
applyI18n();
// Méta nav
const d = new Date(DATA.meta.run_date);
const locale = I18N.date_locale || 'fr-FR';
const fmt = d.toLocaleDateString(locale, { year:'numeric', month:'short', day:'numeric' });
document.getElementById('nav-meta').textContent =
DATA.meta.corpus_name + ' · ' + fmt;
document.getElementById('footer-date').textContent =
(I18N.footer_generated || 'Rapport généré le') + ' ' + fmt;
// Sélecteur moteur galerie
const sel = document.getElementById('gallery-engine-select');
DATA.engines.forEach(e => {
const opt = document.createElement('option');
opt.value = e.name; opt.textContent = e.name;
sel.appendChild(opt);
});
renderRanking();
renderGallery();
buildDocList();
// Restaurer l'état depuis l'URL
const { view, params } = readURLState();
if (view && view !== 'ranking') {
_origShowView(view); // appel direct pour ne pas écraser l'URL
if (view === 'document' && params.doc) {
loadDocument(params.doc);
}
}
// Gérer le bouton retour
window.addEventListener('popstate', () => {
const { view: v, params: p } = readURLState();
_origShowView(v || 'ranking');
if ((v === 'document') && p.doc) loadDocument(p.doc);
});
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>