parse-bench 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. parse_bench/__init__.py +3 -0
  2. parse_bench/analysis/__init__.py +6 -0
  3. parse_bench/analysis/aggregation_report.py +582 -0
  4. parse_bench/analysis/cli.py +472 -0
  5. parse_bench/analysis/comparison.py +382 -0
  6. parse_bench/analysis/comparison_core.py +357 -0
  7. parse_bench/analysis/comparison_report.py +2066 -0
  8. parse_bench/analysis/detailed_report.py +2254 -0
  9. parse_bench/analysis/leaderboard_report.py +852 -0
  10. parse_bench/analysis/metric_definitions.py +771 -0
  11. parse_bench/cli.py +267 -0
  12. parse_bench/data/__init__.py +1 -0
  13. parse_bench/data/cli.py +118 -0
  14. parse_bench/data/download.py +127 -0
  15. parse_bench/evaluation/__init__.py +11 -0
  16. parse_bench/evaluation/cli.py +435 -0
  17. parse_bench/evaluation/evaluators/__init__.py +17 -0
  18. parse_bench/evaluation/evaluators/base.py +34 -0
  19. parse_bench/evaluation/evaluators/extract.py +429 -0
  20. parse_bench/evaluation/evaluators/layoutdet.py +1682 -0
  21. parse_bench/evaluation/evaluators/parse.py +1353 -0
  22. parse_bench/evaluation/evaluators/qa.py +199 -0
  23. parse_bench/evaluation/layout_adapters/__init__.py +21 -0
  24. parse_bench/evaluation/layout_adapters/adapters.py +3180 -0
  25. parse_bench/evaluation/layout_adapters/base.py +105 -0
  26. parse_bench/evaluation/layout_adapters/registry.py +109 -0
  27. parse_bench/evaluation/layout_label_mappers/__init__.py +22 -0
  28. parse_bench/evaluation/layout_label_mappers/base.py +66 -0
  29. parse_bench/evaluation/layout_label_mappers/mappers.py +332 -0
  30. parse_bench/evaluation/layout_label_mappers/projection.py +74 -0
  31. parse_bench/evaluation/layout_label_mappers/registry.py +119 -0
  32. parse_bench/evaluation/metric_aggregation.py +56 -0
  33. parse_bench/evaluation/metrics/__init__.py +5 -0
  34. parse_bench/evaluation/metrics/attribution/__init__.py +35 -0
  35. parse_bench/evaluation/metrics/attribution/constants.py +12 -0
  36. parse_bench/evaluation/metrics/attribution/core.py +1108 -0
  37. parse_bench/evaluation/metrics/attribution/evaluate.py +446 -0
  38. parse_bench/evaluation/metrics/attribution/geometry.py +161 -0
  39. parse_bench/evaluation/metrics/attribution/text_utils.py +233 -0
  40. parse_bench/evaluation/metrics/base.py +33 -0
  41. parse_bench/evaluation/metrics/downstream/__init__.py +0 -0
  42. parse_bench/evaluation/metrics/extract/__init__.py +29 -0
  43. parse_bench/evaluation/metrics/extract/json_subset_match.py +473 -0
  44. parse_bench/evaluation/metrics/extract/json_subset_match_metric.py +81 -0
  45. parse_bench/evaluation/metrics/extract/list_unwrap.py +340 -0
  46. parse_bench/evaluation/metrics/extract/rule_based_metric.py +90 -0
  47. parse_bench/evaluation/metrics/extract/test_rules.py +409 -0
  48. parse_bench/evaluation/metrics/extract/test_types.py +11 -0
  49. parse_bench/evaluation/metrics/field_grounding/__init__.py +21 -0
  50. parse_bench/evaluation/metrics/field_grounding/core.py +437 -0
  51. parse_bench/evaluation/metrics/field_grounding/extract_adapter.py +1224 -0
  52. parse_bench/evaluation/metrics/field_grounding/parse_adapter.py +697 -0
  53. parse_bench/evaluation/metrics/field_grounding/rule_filters.py +19 -0
  54. parse_bench/evaluation/metrics/field_grounding/value_compare.py +190 -0
  55. parse_bench/evaluation/metrics/layoutdet/__init__.py +17 -0
  56. parse_bench/evaluation/metrics/layoutdet/classification_utils.py +300 -0
  57. parse_bench/evaluation/metrics/layoutdet/iou.py +76 -0
  58. parse_bench/evaluation/metrics/parse/__init__.py +5 -0
  59. parse_bench/evaluation/metrics/parse/_vendor_grits_reference.py +531 -0
  60. parse_bench/evaluation/metrics/parse/cross_page_table_consistency.py +165 -0
  61. parse_bench/evaluation/metrics/parse/emphasis_spans.py +242 -0
  62. parse_bench/evaluation/metrics/parse/fast_tree_edit.py +282 -0
  63. parse_bench/evaluation/metrics/parse/grits_metric.py +1125 -0
  64. parse_bench/evaluation/metrics/parse/grits_reference_metric.py +142 -0
  65. parse_bench/evaluation/metrics/parse/header_accuracy_metric.py +1662 -0
  66. parse_bench/evaluation/metrics/parse/llm_normalization/__init__.py +51 -0
  67. parse_bench/evaluation/metrics/parse/llm_normalization/base.py +125 -0
  68. parse_bench/evaluation/metrics/parse/llm_normalization/config.py +44 -0
  69. parse_bench/evaluation/metrics/parse/llm_normalization/postprocess.py +322 -0
  70. parse_bench/evaluation/metrics/parse/llm_normalization/strategy_judge.py +541 -0
  71. parse_bench/evaluation/metrics/parse/mermaid_graph.py +682 -0
  72. parse_bench/evaluation/metrics/parse/rule_based_judge_metric.py +56 -0
  73. parse_bench/evaluation/metrics/parse/rule_based_metric.py +434 -0
  74. parse_bench/evaluation/metrics/parse/rules_bag.py +1161 -0
  75. parse_bench/evaluation/metrics/parse/rules_base.py +751 -0
  76. parse_bench/evaluation/metrics/parse/rules_chart.py +1556 -0
  77. parse_bench/evaluation/metrics/parse/rules_diagram.py +591 -0
  78. parse_bench/evaluation/metrics/parse/rules_form.py +2274 -0
  79. parse_bench/evaluation/metrics/parse/rules_formatting.py +1500 -0
  80. parse_bench/evaluation/metrics/parse/rules_heading.py +228 -0
  81. parse_bench/evaluation/metrics/parse/rules_list.py +226 -0
  82. parse_bench/evaluation/metrics/parse/rules_page_decoration.py +276 -0
  83. parse_bench/evaluation/metrics/parse/rules_table.py +1666 -0
  84. parse_bench/evaluation/metrics/parse/rules_text.py +340 -0
  85. parse_bench/evaluation/metrics/parse/rules_watermark.py +105 -0
  86. parse_bench/evaluation/metrics/parse/structural_consistency_metric.py +251 -0
  87. parse_bench/evaluation/metrics/parse/table_extraction.py +152 -0
  88. parse_bench/evaluation/metrics/parse/table_merging.py +195 -0
  89. parse_bench/evaluation/metrics/parse/table_pairing.py +87 -0
  90. parse_bench/evaluation/metrics/parse/table_parsing.py +955 -0
  91. parse_bench/evaluation/metrics/parse/table_record_match_metric.py +1453 -0
  92. parse_bench/evaluation/metrics/parse/table_splitting.py +301 -0
  93. parse_bench/evaluation/metrics/parse/table_title_stripping.py +530 -0
  94. parse_bench/evaluation/metrics/parse/teds_metric.py +600 -0
  95. parse_bench/evaluation/metrics/parse/test_rules.py +120 -0
  96. parse_bench/evaluation/metrics/parse/test_types.py +103 -0
  97. parse_bench/evaluation/metrics/parse/text_content_projection.py +175 -0
  98. parse_bench/evaluation/metrics/parse/text_similarity_metric.py +61 -0
  99. parse_bench/evaluation/metrics/parse/utils.py +885 -0
  100. parse_bench/evaluation/metrics/qa/__init__.py +5 -0
  101. parse_bench/evaluation/metrics/qa/answer_comparison.py +380 -0
  102. parse_bench/evaluation/qa/__init__.py +5 -0
  103. parse_bench/evaluation/qa/llm_service.py +335 -0
  104. parse_bench/evaluation/reports/__init__.py +8 -0
  105. parse_bench/evaluation/reports/csv.py +64 -0
  106. parse_bench/evaluation/reports/html.py +338 -0
  107. parse_bench/evaluation/reports/markdown.py +98 -0
  108. parse_bench/evaluation/reports/rule_csv.py +22 -0
  109. parse_bench/evaluation/runner.py +1864 -0
  110. parse_bench/evaluation/stats.py +104 -0
  111. parse_bench/extensions.py +72 -0
  112. parse_bench/inference/__init__.py +33 -0
  113. parse_bench/inference/chunkr_layout_extraction.py +160 -0
  114. parse_bench/inference/cli.py +484 -0
  115. parse_bench/inference/layout_extraction.py +422 -0
  116. parse_bench/inference/pipelines/__init__.py +59 -0
  117. parse_bench/inference/pipelines/extract.py +39 -0
  118. parse_bench/inference/pipelines/layout.py +142 -0
  119. parse_bench/inference/pipelines/parse.py +2603 -0
  120. parse_bench/inference/pipelines.py +0 -0
  121. parse_bench/inference/providers/__init__.py +28 -0
  122. parse_bench/inference/providers/base.py +196 -0
  123. parse_bench/inference/providers/cancellation.py +137 -0
  124. parse_bench/inference/providers/extract/__init__.py +22 -0
  125. parse_bench/inference/providers/extract/citations.py +549 -0
  126. parse_bench/inference/providers/extract/extend.py +851 -0
  127. parse_bench/inference/providers/extract/llamaextract_v2_api.py +583 -0
  128. parse_bench/inference/providers/layoutdet/__init__.py +25 -0
  129. parse_bench/inference/providers/layoutdet/adapters.py +946 -0
  130. parse_bench/inference/providers/layoutdet/base.py +203 -0
  131. parse_bench/inference/providers/layoutdet/chandra.py +449 -0
  132. parse_bench/inference/providers/layoutdet/docling.py +125 -0
  133. parse_bench/inference/providers/layoutdet/dots_ocr.py +606 -0
  134. parse_bench/inference/providers/layoutdet/layout_v3.py +137 -0
  135. parse_bench/inference/providers/layoutdet/layout_v3_byoc.py +204 -0
  136. parse_bench/inference/providers/layoutdet/paddle.py +117 -0
  137. parse_bench/inference/providers/layoutdet/qwen3vl.py +360 -0
  138. parse_bench/inference/providers/layoutdet/surya.py +250 -0
  139. parse_bench/inference/providers/layoutdet/yolo.py +109 -0
  140. parse_bench/inference/providers/parse/__init__.py +64 -0
  141. parse_bench/inference/providers/parse/_docling_common.py +233 -0
  142. parse_bench/inference/providers/parse/_layout_utils.py +611 -0
  143. parse_bench/inference/providers/parse/amazon_nova.py +515 -0
  144. parse_bench/inference/providers/parse/anthropic.py +882 -0
  145. parse_bench/inference/providers/parse/azure_document_intelligence.py +700 -0
  146. parse_bench/inference/providers/parse/chandra2.py +633 -0
  147. parse_bench/inference/providers/parse/chunkr.py +268 -0
  148. parse_bench/inference/providers/parse/databricks_ai_parse.py +724 -0
  149. parse_bench/inference/providers/parse/datalab.py +370 -0
  150. parse_bench/inference/providers/parse/deepseekocr2.py +382 -0
  151. parse_bench/inference/providers/parse/docling.py +281 -0
  152. parse_bench/inference/providers/parse/docling_serve.py +289 -0
  153. parse_bench/inference/providers/parse/dots_ocr.py +574 -0
  154. parse_bench/inference/providers/parse/extend_parse.py +710 -0
  155. parse_bench/inference/providers/parse/falconocr.py +436 -0
  156. parse_bench/inference/providers/parse/florin_parser_nano.py +559 -0
  157. parse_bench/inference/providers/parse/gemma4.py +472 -0
  158. parse_bench/inference/providers/parse/glm_zai.py +229 -0
  159. parse_bench/inference/providers/parse/google.py +1125 -0
  160. parse_bench/inference/providers/parse/google_agentic_vision.py +819 -0
  161. parse_bench/inference/providers/parse/google_docai.py +776 -0
  162. parse_bench/inference/providers/parse/google_docai_layout_normalization.py +573 -0
  163. parse_bench/inference/providers/parse/granite_vision.py +515 -0
  164. parse_bench/inference/providers/parse/infinity_parser2.py +704 -0
  165. parse_bench/inference/providers/parse/kdl_frontier_nano.py +3327 -0
  166. parse_bench/inference/providers/parse/landingai.py +452 -0
  167. parse_bench/inference/providers/parse/liteparse.py +350 -0
  168. parse_bench/inference/providers/parse/llamaparse.py +677 -0
  169. parse_bench/inference/providers/parse/llamaparse_v2_normalization.py +1013 -0
  170. parse_bench/inference/providers/parse/markitdown.py +138 -0
  171. parse_bench/inference/providers/parse/mineru25.py +405 -0
  172. parse_bench/inference/providers/parse/mineru2605pro.py +432 -0
  173. parse_bench/inference/providers/parse/mineru_diffusion.py +371 -0
  174. parse_bench/inference/providers/parse/mistral_ocr.py +546 -0
  175. parse_bench/inference/providers/parse/nemotron_omni.py +473 -0
  176. parse_bench/inference/providers/parse/oi_parser.py +222 -0
  177. parse_bench/inference/providers/parse/openai.py +740 -0
  178. parse_bench/inference/providers/parse/opendataloader.py +152 -0
  179. parse_bench/inference/providers/parse/paddleocr.py +624 -0
  180. parse_bench/inference/providers/parse/pdf_inspector.py +142 -0
  181. parse_bench/inference/providers/parse/pulse.py +785 -0
  182. parse_bench/inference/providers/parse/pymupdf.py +207 -0
  183. parse_bench/inference/providers/parse/pymupdf4llm.py +356 -0
  184. parse_bench/inference/providers/parse/pypdf.py +179 -0
  185. parse_bench/inference/providers/parse/qwen.py +678 -0
  186. parse_bench/inference/providers/parse/rakedoc_nano.py +70 -0
  187. parse_bench/inference/providers/parse/reducto.py +546 -0
  188. parse_bench/inference/providers/parse/surya2.py +372 -0
  189. parse_bench/inference/providers/parse/tesseract.py +301 -0
  190. parse_bench/inference/providers/parse/textract.py +694 -0
  191. parse_bench/inference/providers/parse/unlimitedocr.py +346 -0
  192. parse_bench/inference/providers/parse/unstructured.py +485 -0
  193. parse_bench/inference/providers/parse/warp_ingest.py +199 -0
  194. parse_bench/inference/providers/registry.py +49 -0
  195. parse_bench/inference/renormalize.py +170 -0
  196. parse_bench/inference/runner.py +2023 -0
  197. parse_bench/layout_label_mapping.py +424 -0
  198. parse_bench/layout_projection.py +179 -0
  199. parse_bench/pipeline/__init__.py +1 -0
  200. parse_bench/pipeline/cli.py +549 -0
  201. parse_bench/schemas/__init__.py +33 -0
  202. parse_bench/schemas/evaluation.py +93 -0
  203. parse_bench/schemas/extract_output.py +36 -0
  204. parse_bench/schemas/layout_detection_output.py +545 -0
  205. parse_bench/schemas/layout_ontology.py +315 -0
  206. parse_bench/schemas/metrics.py +69 -0
  207. parse_bench/schemas/parse_output.py +152 -0
  208. parse_bench/schemas/pipeline.py +22 -0
  209. parse_bench/schemas/pipeline_io.py +106 -0
  210. parse_bench/schemas/product.py +97 -0
  211. parse_bench/test_cases/__init__.py +25 -0
  212. parse_bench/test_cases/bbox_value_strict_comparator.py +880 -0
  213. parse_bench/test_cases/extract_field_paths.py +164 -0
  214. parse_bench/test_cases/layout_attribution_generation.py +287 -0
  215. parse_bench/test_cases/loader.py +652 -0
  216. parse_bench/test_cases/parse_rule_schemas.py +1071 -0
  217. parse_bench/test_cases/rule_filters.py +32 -0
  218. parse_bench/test_cases/rule_ids.py +107 -0
  219. parse_bench/test_cases/schema.py +427 -0
  220. parse_bench/utils/__init__.py +15 -0
  221. parse_bench/utils/gemini_layout_utils.py +670 -0
  222. parse_bench/utils/text_aggregation.py +100 -0
  223. parse_bench-1.0.0.dist-info/METADATA +476 -0
  224. parse_bench-1.0.0.dist-info/RECORD +227 -0
  225. parse_bench-1.0.0.dist-info/WHEEL +4 -0
  226. parse_bench-1.0.0.dist-info/entry_points.txt +2 -0
  227. parse_bench-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,3 @@
1
+ """Document parsing evaluation system for competitive benchmarking."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,6 @@
1
+ """Analysis tools for comparing and analyzing pipeline results."""
2
+
3
+ from parse_bench.analysis.comparison import PipelineComparison
4
+ from parse_bench.analysis.comparison_report import generate_comparison_html
5
+
6
+ __all__ = ["PipelineComparison", "generate_comparison_html"]
@@ -0,0 +1,582 @@
1
+ """Aggregation dashboard report for multi-category benchmark runs.
2
+
3
+ Generates a self-contained HTML dashboard showing all categories side-by-side,
4
+ with per-category metric selectors, pipeline metadata, and links to detailed reports.
5
+
6
+ Uses the same design system (Newsreader / Plus Jakarta Sans / JetBrains Mono,
7
+ warm editorial palette) as the detailed evaluation reports.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from datetime import UTC, datetime
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from parse_bench.analysis.metric_definitions import (
18
+ TOOLTIP_CSS,
19
+ TOOLTIP_JS,
20
+ display_name,
21
+ tooltip_dict,
22
+ )
23
+ from parse_bench.schemas.evaluation import EvaluationSummary
24
+
25
+
26
+ def _load_category_summary(report_json: Path) -> EvaluationSummary | None:
27
+ """Load an EvaluationSummary from a per-category report JSON."""
28
+ try:
29
+ data = json.loads(report_json.read_text(encoding="utf-8"))
30
+ return EvaluationSummary.model_validate(data)
31
+ except Exception:
32
+ return None
33
+
34
+
35
+ # Default "main metric" per category type. Everything else falls back to rule_pass_rate.
36
+ _DEFAULT_METRICS: dict[str, str] = {
37
+ "table": "grits_trm_composite",
38
+ "layout": "layout_element_rule_pass_rate",
39
+ "text_content": "content_faithfulness",
40
+ "text_formatting": "semantic_formatting",
41
+ "form": "rule_form_field_pass_rate",
42
+ }
43
+
44
+
45
+ def _extract_category_data(name: str, summary: EvaluationSummary) -> dict[str, Any]:
46
+ """Extract display data for a single category from its EvaluationSummary."""
47
+ metrics = summary.aggregate_metrics
48
+
49
+ # Build metric list from avg_* keys only
50
+ metric_list: list[dict[str, Any]] = []
51
+ for key in sorted(metrics.keys()):
52
+ if not key.startswith("avg_"):
53
+ continue
54
+ metric_name = key[len("avg_") :]
55
+ # Skip _predicted duplicates and _judge duplicates
56
+ if "_predicted" in metric_name or "_judge" in metric_name:
57
+ continue
58
+ metric_list.append(
59
+ {
60
+ "name": metric_name,
61
+ "displayName": display_name(metric_name),
62
+ "value": metrics[key], # raw 0-1 float
63
+ }
64
+ )
65
+
66
+ # Determine default metric for this category
67
+ default_metric = _DEFAULT_METRICS.get(name, "rule_pass_rate")
68
+ # Fall back if default isn't available in the metrics list
69
+ metric_names_set = {m["name"] for m in metric_list}
70
+ if default_metric not in metric_names_set:
71
+ if "rule_pass_rate" in metric_names_set:
72
+ default_metric = "rule_pass_rate"
73
+ else:
74
+ default_metric = metric_list[0]["name"] if metric_list else ""
75
+
76
+ return {
77
+ "name": name,
78
+ "displayName": name.replace("_", " ").title(),
79
+ "files": summary.total_examples,
80
+ "defaultMetric": default_metric,
81
+ "metrics": metric_list,
82
+ }
83
+
84
+
85
+ def generate_aggregation_report(
86
+ pipeline_output_dir: Path,
87
+ groups: list[str],
88
+ pipeline_name: str = "",
89
+ ) -> Path:
90
+ """Generate an aggregation dashboard HTML showing all categories side-by-side.
91
+
92
+ Args:
93
+ pipeline_output_dir: Directory containing per-category subdirectories with
94
+ _evaluation_report.json files.
95
+ groups: List of category/group names to include.
96
+ pipeline_name: Pipeline name for display in the report header.
97
+
98
+ Returns:
99
+ Path to the generated HTML file.
100
+ """
101
+ # Load pipeline metadata
102
+ pipeline_metadata: dict[str, Any] = {}
103
+ metadata_path = pipeline_output_dir / "_metadata.json"
104
+ if metadata_path.exists():
105
+ try:
106
+ pipeline_metadata = json.loads(metadata_path.read_text(encoding="utf-8")).get("pipeline", {})
107
+ except Exception:
108
+ pass
109
+
110
+ if not pipeline_name and pipeline_metadata.get("pipeline_name"):
111
+ pipeline_name = pipeline_metadata["pipeline_name"]
112
+
113
+ categories: list[dict[str, Any]] = []
114
+ for group_name in groups:
115
+ report_path = pipeline_output_dir / group_name / "_evaluation_report.json"
116
+ summary = _load_category_summary(report_path)
117
+ if summary is not None:
118
+ cat_data = _extract_category_data(group_name, summary)
119
+ categories.append(cat_data)
120
+
121
+ total_files = sum(c["files"] for c in categories)
122
+
123
+ data_blob = {
124
+ "pipelineName": pipeline_name,
125
+ "pipelineMetadata": pipeline_metadata,
126
+ "generatedAt": datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"),
127
+ "totalFiles": total_files,
128
+ "categories": categories,
129
+ "metricTooltips": tooltip_dict(),
130
+ }
131
+
132
+ data_json = json.dumps(data_blob, default=str, ensure_ascii=False)
133
+ data_json = data_json.replace("</script>", "<\\/script>")
134
+ data_json = data_json.replace("<!--", "<\\!--")
135
+
136
+ # Build full HTML by concatenation (same pattern as detailed_report.py)
137
+ parts: list[str] = []
138
+ parts.append(_HTML_HEAD)
139
+ parts.append(_CSS)
140
+ parts.append("</style>\n</head>\n")
141
+ parts.append(_HTML_BODY)
142
+
143
+ # Data blob
144
+ parts.append("\n<script>\nconst DATA = ")
145
+ parts.append(data_json)
146
+ parts.append(";\n</script>\n")
147
+
148
+ # Application JS
149
+ parts.append("<script>\n")
150
+ parts.append(_JS)
151
+ parts.append("\n</script>\n")
152
+ parts.append("</body>\n</html>\n")
153
+
154
+ html = "".join(parts)
155
+ output_path = pipeline_output_dir / "_evaluation_report_dashboard.html"
156
+ output_path.write_text(html, encoding="utf-8")
157
+ return output_path
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # HTML template parts — uses same design system as detailed_report.py
162
+ # ---------------------------------------------------------------------------
163
+
164
+ _FONT_URL = (
165
+ "https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@"
166
+ "0,6..72,400;0,6..72,600;0,6..72,700;1,6..72,400"
167
+ "&family=Plus+Jakarta+Sans:wght@400;500;600;700"
168
+ "&family=JetBrains+Mono:wght@400;500&display=swap"
169
+ )
170
+
171
+ _HTML_HEAD = f"""\
172
+ <!DOCTYPE html>
173
+ <html lang="en">
174
+ <head>
175
+ <meta charset="UTF-8">
176
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
177
+ <title>Evaluation Report</title>
178
+ <link rel="preconnect" href="https://fonts.googleapis.com">
179
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
180
+ <link href="{_FONT_URL}" rel="stylesheet">
181
+ <style>
182
+ """
183
+
184
+ _CSS = (
185
+ """\
186
+ /* ───── Reset & variables (shared with detailed_report.py) ───── */
187
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
188
+ :root {
189
+ --bg: #f8f7f4;
190
+ --fg: #1c1917;
191
+ --card: #ffffff;
192
+ --border: #e7e5e4;
193
+ --muted: #78716c;
194
+ --muted-light: #a8a29e;
195
+ --cream: #faf9f6;
196
+ --emerald: #059669;
197
+ --emerald-bg: #ecfdf5;
198
+ --amber: #d97706;
199
+ --amber-bg: #fffbeb;
200
+ --red: #dc2626;
201
+ --red-bg: #fef2f2;
202
+ --blue: #2563eb;
203
+ --blue-bg: #eff6ff;
204
+ --font-heading: 'Newsreader', Georgia, serif;
205
+ --font-body: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
206
+ --font-mono: 'JetBrains Mono', 'SF Mono', monospace;
207
+ --shadow-sm: 0 1px 2px rgba(28,25,23,0.05);
208
+ --shadow-md: 0 4px 6px -1px rgba(28,25,23,0.07), 0 2px 4px -2px rgba(28,25,23,0.05);
209
+ --radius: 10px;
210
+ --radius-sm: 6px;
211
+ }
212
+ html { font-size: 15px; }
213
+ body {
214
+ font-family: var(--font-body);
215
+ background: var(--bg);
216
+ color: var(--fg);
217
+ line-height: 1.6;
218
+ -webkit-font-smoothing: antialiased;
219
+ }
220
+
221
+ /* ───── Scrollbar ───── */
222
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
223
+ ::-webkit-scrollbar-track { background: var(--cream); }
224
+ ::-webkit-scrollbar-thumb { background: var(--muted-light); border-radius: 4px; }
225
+ ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
226
+
227
+ /* ───── Layout ───── */
228
+ .report-container {
229
+ max-width: 1340px;
230
+ margin: 0 auto;
231
+ padding: 32px 24px 64px;
232
+ }
233
+
234
+ /* ───── Header ───── */
235
+ .report-header {
236
+ margin-bottom: 36px;
237
+ }
238
+ .report-header h1 {
239
+ font-family: var(--font-heading);
240
+ font-size: 2.2rem;
241
+ font-weight: 700;
242
+ letter-spacing: -0.02em;
243
+ color: var(--fg);
244
+ line-height: 1.2;
245
+ }
246
+ .report-header .subtitle {
247
+ font-size: 0.9rem;
248
+ color: var(--muted);
249
+ margin-top: 6px;
250
+ }
251
+
252
+ /* ───── Summary cards ───── */
253
+ .summary-row {
254
+ display: grid;
255
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
256
+ gap: 14px;
257
+ margin-bottom: 32px;
258
+ }
259
+ .summary-card {
260
+ background: var(--card);
261
+ border: 1px solid var(--border);
262
+ border-radius: var(--radius);
263
+ padding: 18px 20px;
264
+ box-shadow: var(--shadow-sm);
265
+ transition: box-shadow 0.15s;
266
+ }
267
+ .summary-card:hover { box-shadow: var(--shadow-md); }
268
+ .summary-card .label {
269
+ font-size: 0.75rem;
270
+ font-weight: 600;
271
+ text-transform: uppercase;
272
+ letter-spacing: 0.06em;
273
+ color: var(--muted);
274
+ margin-bottom: 4px;
275
+ }
276
+ .summary-card .big-number {
277
+ font-family: var(--font-heading);
278
+ font-size: 2rem;
279
+ font-weight: 700;
280
+ line-height: 1.15;
281
+ }
282
+
283
+ /* ───── Section titles ───── */
284
+ .section-title {
285
+ font-family: var(--font-heading);
286
+ font-size: 1.35rem;
287
+ font-weight: 600;
288
+ margin-bottom: 16px;
289
+ padding-bottom: 8px;
290
+ border-bottom: 2px solid var(--border);
291
+ }
292
+
293
+ /* ───── Categories grid ───── */
294
+ .categories-grid {
295
+ display: grid;
296
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
297
+ gap: 14px;
298
+ }
299
+
300
+ .category-card {
301
+ background: var(--card);
302
+ border: 1px solid var(--border);
303
+ border-radius: var(--radius);
304
+ padding: 20px 22px;
305
+ box-shadow: var(--shadow-sm);
306
+ transition: box-shadow 0.15s;
307
+ cursor: pointer;
308
+ display: block;
309
+ color: inherit;
310
+ }
311
+ .category-card:hover { box-shadow: var(--shadow-md); }
312
+ .category-card h3 {
313
+ font-family: var(--font-heading);
314
+ font-size: 1.2rem;
315
+ font-weight: 700;
316
+ margin-bottom: 8px;
317
+ color: var(--fg);
318
+ }
319
+ .category-card h3 .file-count {
320
+ font-size: 0.8rem;
321
+ font-weight: 400;
322
+ color: var(--muted);
323
+ }
324
+ .category-card .main-score {
325
+ font-family: var(--font-heading);
326
+ font-size: 2rem;
327
+ font-weight: 700;
328
+ line-height: 1.15;
329
+ margin-bottom: 8px;
330
+ }
331
+ .color-emerald { color: var(--emerald); }
332
+ .color-amber { color: var(--amber); }
333
+ .color-red { color: var(--red); }
334
+
335
+ /* ───── Progress bar ───── */
336
+ .progress-bar-track {
337
+ height: 6px;
338
+ background: var(--cream);
339
+ border-radius: 3px;
340
+ overflow: hidden;
341
+ margin-bottom: 12px;
342
+ }
343
+ .progress-bar-fill {
344
+ height: 100%;
345
+ border-radius: 3px;
346
+ transition: width 0.3s ease;
347
+ }
348
+ .bar-emerald { background: var(--emerald); }
349
+ .bar-amber { background: var(--amber); }
350
+ .bar-red { background: var(--red); }
351
+
352
+ /* ───── Metric selector dropdown ───── */
353
+ .metric-selector {
354
+ width: 100%;
355
+ margin-bottom: 10px;
356
+ padding: 6px 8px;
357
+ font-family: var(--font-body);
358
+ font-size: 0.8rem;
359
+ border: 1px solid var(--border);
360
+ border-radius: var(--radius-sm);
361
+ background: var(--cream);
362
+ color: var(--fg);
363
+ cursor: pointer;
364
+ outline: none;
365
+ transition: border-color 0.15s;
366
+ }
367
+ .metric-selector:focus { border-color: var(--blue); }
368
+
369
+ /* ───── Metric rows ───── */
370
+ .metric-row {
371
+ display: flex;
372
+ justify-content: space-between;
373
+ align-items: baseline;
374
+ padding: 5px 0;
375
+ font-size: 0.82rem;
376
+ border-bottom: 1px solid var(--cream);
377
+ }
378
+ .metric-row:last-child { border-bottom: none; }
379
+ .metric-row .metric-name {
380
+ color: var(--muted);
381
+ flex: 1;
382
+ min-width: 0;
383
+ line-height: 1.4;
384
+ }
385
+ .metric-row .metric-value {
386
+ font-family: var(--font-mono);
387
+ font-size: 0.8rem;
388
+ font-weight: 500;
389
+ margin-left: 8px;
390
+ white-space: nowrap;
391
+ flex-shrink: 0;
392
+ }
393
+ .metric-row.selected .metric-name {
394
+ font-weight: 700;
395
+ color: var(--fg);
396
+ }
397
+
398
+ /* ───── Responsive ───── */
399
+ @media (max-width: 900px) {
400
+ .summary-row { grid-template-columns: repeat(2, 1fr); }
401
+ .categories-grid { grid-template-columns: 1fr; }
402
+ }
403
+ @media (max-width: 600px) {
404
+ .report-container { padding: 16px 12px 48px; }
405
+ .summary-row { grid-template-columns: 1fr; }
406
+ }
407
+ """
408
+ + TOOLTIP_CSS
409
+ )
410
+
411
+ _HTML_BODY = """\
412
+ <body>
413
+ <div class="report-container">
414
+ <header class="report-header">
415
+ <h1 id="report-title">Evaluation Report</h1>
416
+ <p class="subtitle" id="subtitle"></p>
417
+ </header>
418
+
419
+ <div class="summary-row" id="summary-cards"></div>
420
+
421
+ <h2 class="section-title">Categories</h2>
422
+ <div class="categories-grid" id="categories-grid"></div>
423
+ </div>
424
+ """
425
+
426
+ _JS = (
427
+ """\
428
+ (function() {
429
+ function colorClass(rate) {
430
+ if (rate >= 80) return 'emerald';
431
+ if (rate >= 50) return 'amber';
432
+ return 'red';
433
+ }
434
+
435
+ function pct(val, d) {
436
+ d = d !== undefined ? d : 1;
437
+ return val.toFixed(d) + '%';
438
+ }
439
+
440
+ function esc(s) {
441
+ if (s == null) return '';
442
+ var d = document.createElement('div');
443
+ d.textContent = String(s);
444
+ return d.innerHTML;
445
+ }
446
+
447
+ """
448
+ + TOOLTIP_JS
449
+ + """
450
+
451
+ // ─── State: selected metric per category ───
452
+ var selectedMetrics = {};
453
+ DATA.categories.forEach(function(cat) {
454
+ selectedMetrics[cat.name] = cat.defaultMetric;
455
+ });
456
+
457
+ // ─── Header ───
458
+ var titleText = DATA.pipelineName
459
+ ? DATA.pipelineName.replace(/_/g, ' ').replace(/\\b\\w/g, function(c) { return c.toUpperCase(); })
460
+ : 'Evaluation Report';
461
+ document.getElementById('report-title').textContent = titleText;
462
+ document.getElementById('subtitle').textContent = 'Generated: ' + DATA.generatedAt;
463
+
464
+ // ─── Helpers ───
465
+ function getSelectedValue(cat) {
466
+ var sel = selectedMetrics[cat.name];
467
+ for (var i = 0; i < cat.metrics.length; i++) {
468
+ if (cat.metrics[i].name === sel) return cat.metrics[i].value * 100;
469
+ }
470
+ return 0;
471
+ }
472
+
473
+ function computeAvgScore() {
474
+ var sum = 0, count = 0;
475
+ DATA.categories.forEach(function(cat) {
476
+ sum += getSelectedValue(cat);
477
+ count++;
478
+ });
479
+ return count > 0 ? sum / count : 0;
480
+ }
481
+
482
+ // ─── Render summary cards ───
483
+ function renderSummary() {
484
+ var sc = document.getElementById('summary-cards');
485
+ sc.innerHTML = '';
486
+ var avg = computeAvgScore();
487
+ var ac = colorClass(avg);
488
+
489
+ // Total Files card
490
+ var d1 = document.createElement('div');
491
+ d1.className = 'summary-card';
492
+ d1.innerHTML = '<div class="label">TOTAL FILES</div><div class="big-number">' + DATA.totalFiles + '</div>';
493
+ sc.appendChild(d1);
494
+
495
+ // Avg Score card
496
+ var d2 = document.createElement('div');
497
+ d2.className = 'summary-card';
498
+ d2.innerHTML = '<div class="label">AVG SCORE</div><div class="big-number color-' + ac + '">' + pct(avg) + '</div>';
499
+ sc.appendChild(d2);
500
+ }
501
+
502
+ // ─── Render category cards ───
503
+ function renderCategories() {
504
+ var grid = document.getElementById('categories-grid');
505
+ grid.innerHTML = '';
506
+ // Set grid columns to match number of categories
507
+ var numCats = DATA.categories.length;
508
+ grid.style.gridTemplateColumns = 'repeat(' + numCats + ', 1fr)';
509
+
510
+ DATA.categories.forEach(function(cat) {
511
+ var card = document.createElement('div');
512
+ card.className = 'category-card';
513
+
514
+ var selMetric = selectedMetrics[cat.name];
515
+ var mainVal = getSelectedValue(cat);
516
+ var c = colorClass(mainVal);
517
+
518
+ var html = '<h3>' + esc(cat.displayName) + ' <span class="file-count">(' + cat.files + ' files)</span></h3>';
519
+ html += '<div class="main-score color-' + c + '">' + pct(mainVal) + '</div>';
520
+ html += '<div class="progress-bar-track"><div class="progress-bar-fill bar-' + c
521
+ + '" style="width:' + Math.min(mainVal, 100) + '%"></div></div>';
522
+
523
+ // Metric selector dropdown
524
+ html += '<select class="metric-selector" data-cat="' + esc(cat.name) + '">';
525
+ for (var i = 0; i < cat.metrics.length; i++) {
526
+ var m = cat.metrics[i];
527
+ var selected = m.name === selMetric ? ' selected' : '';
528
+ html += '<option value="' + esc(m.name) + '"' + selected + '>' + esc(m.displayName) + '</option>';
529
+ }
530
+ html += '</select>';
531
+
532
+ // Metric list: selected metric first, then the rest in original order
533
+ var sorted = [];
534
+ var rest = [];
535
+ for (var j = 0; j < cat.metrics.length; j++) {
536
+ if (cat.metrics[j].name === selMetric) {
537
+ sorted.unshift(cat.metrics[j]);
538
+ } else {
539
+ rest.push(cat.metrics[j]);
540
+ }
541
+ }
542
+ sorted = sorted.concat(rest);
543
+
544
+ for (var k = 0; k < sorted.length; k++) {
545
+ var sm = sorted[k];
546
+ var mc = colorClass(sm.value * 100);
547
+ var selClass = sm.name === selMetric ? ' selected' : '';
548
+ html += '<div class="metric-row' + selClass + '">';
549
+ html += '<span class="metric-name">' + esc(sm.displayName) + '</span>' + tooltipIcon(sm.name);
550
+ html += '<span class="metric-value color-' + mc + '">' + pct(sm.value * 100) + '</span>';
551
+ html += '</div>';
552
+ }
553
+
554
+ card.innerHTML = html;
555
+
556
+ // Click on card navigates to detailed report (but not when clicking dropdown)
557
+ card.addEventListener('click', function(e) {
558
+ if (e.target.tagName === 'SELECT' || e.target.tagName === 'OPTION') return;
559
+ if (e.target.closest && e.target.closest('.metric-hint')) return;
560
+ window.location.href = cat.name + '/_evaluation_report_detailed.html';
561
+ });
562
+
563
+ // Dropdown change updates selected metric and re-renders
564
+ var select = card.querySelector('.metric-selector');
565
+ if (select) {
566
+ select.addEventListener('change', function(e) {
567
+ e.stopPropagation();
568
+ selectedMetrics[cat.name] = e.target.value;
569
+ renderCategories();
570
+ renderSummary();
571
+ });
572
+ }
573
+
574
+ grid.appendChild(card);
575
+ });
576
+ }
577
+
578
+ renderSummary();
579
+ renderCategories();
580
+ })();
581
+ """
582
+ )