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.
- parse_bench/__init__.py +3 -0
- parse_bench/analysis/__init__.py +6 -0
- parse_bench/analysis/aggregation_report.py +582 -0
- parse_bench/analysis/cli.py +472 -0
- parse_bench/analysis/comparison.py +382 -0
- parse_bench/analysis/comparison_core.py +357 -0
- parse_bench/analysis/comparison_report.py +2066 -0
- parse_bench/analysis/detailed_report.py +2254 -0
- parse_bench/analysis/leaderboard_report.py +852 -0
- parse_bench/analysis/metric_definitions.py +771 -0
- parse_bench/cli.py +267 -0
- parse_bench/data/__init__.py +1 -0
- parse_bench/data/cli.py +118 -0
- parse_bench/data/download.py +127 -0
- parse_bench/evaluation/__init__.py +11 -0
- parse_bench/evaluation/cli.py +435 -0
- parse_bench/evaluation/evaluators/__init__.py +17 -0
- parse_bench/evaluation/evaluators/base.py +34 -0
- parse_bench/evaluation/evaluators/extract.py +429 -0
- parse_bench/evaluation/evaluators/layoutdet.py +1682 -0
- parse_bench/evaluation/evaluators/parse.py +1353 -0
- parse_bench/evaluation/evaluators/qa.py +199 -0
- parse_bench/evaluation/layout_adapters/__init__.py +21 -0
- parse_bench/evaluation/layout_adapters/adapters.py +3180 -0
- parse_bench/evaluation/layout_adapters/base.py +105 -0
- parse_bench/evaluation/layout_adapters/registry.py +109 -0
- parse_bench/evaluation/layout_label_mappers/__init__.py +22 -0
- parse_bench/evaluation/layout_label_mappers/base.py +66 -0
- parse_bench/evaluation/layout_label_mappers/mappers.py +332 -0
- parse_bench/evaluation/layout_label_mappers/projection.py +74 -0
- parse_bench/evaluation/layout_label_mappers/registry.py +119 -0
- parse_bench/evaluation/metric_aggregation.py +56 -0
- parse_bench/evaluation/metrics/__init__.py +5 -0
- parse_bench/evaluation/metrics/attribution/__init__.py +35 -0
- parse_bench/evaluation/metrics/attribution/constants.py +12 -0
- parse_bench/evaluation/metrics/attribution/core.py +1108 -0
- parse_bench/evaluation/metrics/attribution/evaluate.py +446 -0
- parse_bench/evaluation/metrics/attribution/geometry.py +161 -0
- parse_bench/evaluation/metrics/attribution/text_utils.py +233 -0
- parse_bench/evaluation/metrics/base.py +33 -0
- parse_bench/evaluation/metrics/downstream/__init__.py +0 -0
- parse_bench/evaluation/metrics/extract/__init__.py +29 -0
- parse_bench/evaluation/metrics/extract/json_subset_match.py +473 -0
- parse_bench/evaluation/metrics/extract/json_subset_match_metric.py +81 -0
- parse_bench/evaluation/metrics/extract/list_unwrap.py +340 -0
- parse_bench/evaluation/metrics/extract/rule_based_metric.py +90 -0
- parse_bench/evaluation/metrics/extract/test_rules.py +409 -0
- parse_bench/evaluation/metrics/extract/test_types.py +11 -0
- parse_bench/evaluation/metrics/field_grounding/__init__.py +21 -0
- parse_bench/evaluation/metrics/field_grounding/core.py +437 -0
- parse_bench/evaluation/metrics/field_grounding/extract_adapter.py +1224 -0
- parse_bench/evaluation/metrics/field_grounding/parse_adapter.py +697 -0
- parse_bench/evaluation/metrics/field_grounding/rule_filters.py +19 -0
- parse_bench/evaluation/metrics/field_grounding/value_compare.py +190 -0
- parse_bench/evaluation/metrics/layoutdet/__init__.py +17 -0
- parse_bench/evaluation/metrics/layoutdet/classification_utils.py +300 -0
- parse_bench/evaluation/metrics/layoutdet/iou.py +76 -0
- parse_bench/evaluation/metrics/parse/__init__.py +5 -0
- parse_bench/evaluation/metrics/parse/_vendor_grits_reference.py +531 -0
- parse_bench/evaluation/metrics/parse/cross_page_table_consistency.py +165 -0
- parse_bench/evaluation/metrics/parse/emphasis_spans.py +242 -0
- parse_bench/evaluation/metrics/parse/fast_tree_edit.py +282 -0
- parse_bench/evaluation/metrics/parse/grits_metric.py +1125 -0
- parse_bench/evaluation/metrics/parse/grits_reference_metric.py +142 -0
- parse_bench/evaluation/metrics/parse/header_accuracy_metric.py +1662 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/__init__.py +51 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/base.py +125 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/config.py +44 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/postprocess.py +322 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/strategy_judge.py +541 -0
- parse_bench/evaluation/metrics/parse/mermaid_graph.py +682 -0
- parse_bench/evaluation/metrics/parse/rule_based_judge_metric.py +56 -0
- parse_bench/evaluation/metrics/parse/rule_based_metric.py +434 -0
- parse_bench/evaluation/metrics/parse/rules_bag.py +1161 -0
- parse_bench/evaluation/metrics/parse/rules_base.py +751 -0
- parse_bench/evaluation/metrics/parse/rules_chart.py +1556 -0
- parse_bench/evaluation/metrics/parse/rules_diagram.py +591 -0
- parse_bench/evaluation/metrics/parse/rules_form.py +2274 -0
- parse_bench/evaluation/metrics/parse/rules_formatting.py +1500 -0
- parse_bench/evaluation/metrics/parse/rules_heading.py +228 -0
- parse_bench/evaluation/metrics/parse/rules_list.py +226 -0
- parse_bench/evaluation/metrics/parse/rules_page_decoration.py +276 -0
- parse_bench/evaluation/metrics/parse/rules_table.py +1666 -0
- parse_bench/evaluation/metrics/parse/rules_text.py +340 -0
- parse_bench/evaluation/metrics/parse/rules_watermark.py +105 -0
- parse_bench/evaluation/metrics/parse/structural_consistency_metric.py +251 -0
- parse_bench/evaluation/metrics/parse/table_extraction.py +152 -0
- parse_bench/evaluation/metrics/parse/table_merging.py +195 -0
- parse_bench/evaluation/metrics/parse/table_pairing.py +87 -0
- parse_bench/evaluation/metrics/parse/table_parsing.py +955 -0
- parse_bench/evaluation/metrics/parse/table_record_match_metric.py +1453 -0
- parse_bench/evaluation/metrics/parse/table_splitting.py +301 -0
- parse_bench/evaluation/metrics/parse/table_title_stripping.py +530 -0
- parse_bench/evaluation/metrics/parse/teds_metric.py +600 -0
- parse_bench/evaluation/metrics/parse/test_rules.py +120 -0
- parse_bench/evaluation/metrics/parse/test_types.py +103 -0
- parse_bench/evaluation/metrics/parse/text_content_projection.py +175 -0
- parse_bench/evaluation/metrics/parse/text_similarity_metric.py +61 -0
- parse_bench/evaluation/metrics/parse/utils.py +885 -0
- parse_bench/evaluation/metrics/qa/__init__.py +5 -0
- parse_bench/evaluation/metrics/qa/answer_comparison.py +380 -0
- parse_bench/evaluation/qa/__init__.py +5 -0
- parse_bench/evaluation/qa/llm_service.py +335 -0
- parse_bench/evaluation/reports/__init__.py +8 -0
- parse_bench/evaluation/reports/csv.py +64 -0
- parse_bench/evaluation/reports/html.py +338 -0
- parse_bench/evaluation/reports/markdown.py +98 -0
- parse_bench/evaluation/reports/rule_csv.py +22 -0
- parse_bench/evaluation/runner.py +1864 -0
- parse_bench/evaluation/stats.py +104 -0
- parse_bench/extensions.py +72 -0
- parse_bench/inference/__init__.py +33 -0
- parse_bench/inference/chunkr_layout_extraction.py +160 -0
- parse_bench/inference/cli.py +484 -0
- parse_bench/inference/layout_extraction.py +422 -0
- parse_bench/inference/pipelines/__init__.py +59 -0
- parse_bench/inference/pipelines/extract.py +39 -0
- parse_bench/inference/pipelines/layout.py +142 -0
- parse_bench/inference/pipelines/parse.py +2603 -0
- parse_bench/inference/pipelines.py +0 -0
- parse_bench/inference/providers/__init__.py +28 -0
- parse_bench/inference/providers/base.py +196 -0
- parse_bench/inference/providers/cancellation.py +137 -0
- parse_bench/inference/providers/extract/__init__.py +22 -0
- parse_bench/inference/providers/extract/citations.py +549 -0
- parse_bench/inference/providers/extract/extend.py +851 -0
- parse_bench/inference/providers/extract/llamaextract_v2_api.py +583 -0
- parse_bench/inference/providers/layoutdet/__init__.py +25 -0
- parse_bench/inference/providers/layoutdet/adapters.py +946 -0
- parse_bench/inference/providers/layoutdet/base.py +203 -0
- parse_bench/inference/providers/layoutdet/chandra.py +449 -0
- parse_bench/inference/providers/layoutdet/docling.py +125 -0
- parse_bench/inference/providers/layoutdet/dots_ocr.py +606 -0
- parse_bench/inference/providers/layoutdet/layout_v3.py +137 -0
- parse_bench/inference/providers/layoutdet/layout_v3_byoc.py +204 -0
- parse_bench/inference/providers/layoutdet/paddle.py +117 -0
- parse_bench/inference/providers/layoutdet/qwen3vl.py +360 -0
- parse_bench/inference/providers/layoutdet/surya.py +250 -0
- parse_bench/inference/providers/layoutdet/yolo.py +109 -0
- parse_bench/inference/providers/parse/__init__.py +64 -0
- parse_bench/inference/providers/parse/_docling_common.py +233 -0
- parse_bench/inference/providers/parse/_layout_utils.py +611 -0
- parse_bench/inference/providers/parse/amazon_nova.py +515 -0
- parse_bench/inference/providers/parse/anthropic.py +882 -0
- parse_bench/inference/providers/parse/azure_document_intelligence.py +700 -0
- parse_bench/inference/providers/parse/chandra2.py +633 -0
- parse_bench/inference/providers/parse/chunkr.py +268 -0
- parse_bench/inference/providers/parse/databricks_ai_parse.py +724 -0
- parse_bench/inference/providers/parse/datalab.py +370 -0
- parse_bench/inference/providers/parse/deepseekocr2.py +382 -0
- parse_bench/inference/providers/parse/docling.py +281 -0
- parse_bench/inference/providers/parse/docling_serve.py +289 -0
- parse_bench/inference/providers/parse/dots_ocr.py +574 -0
- parse_bench/inference/providers/parse/extend_parse.py +710 -0
- parse_bench/inference/providers/parse/falconocr.py +436 -0
- parse_bench/inference/providers/parse/florin_parser_nano.py +559 -0
- parse_bench/inference/providers/parse/gemma4.py +472 -0
- parse_bench/inference/providers/parse/glm_zai.py +229 -0
- parse_bench/inference/providers/parse/google.py +1125 -0
- parse_bench/inference/providers/parse/google_agentic_vision.py +819 -0
- parse_bench/inference/providers/parse/google_docai.py +776 -0
- parse_bench/inference/providers/parse/google_docai_layout_normalization.py +573 -0
- parse_bench/inference/providers/parse/granite_vision.py +515 -0
- parse_bench/inference/providers/parse/infinity_parser2.py +704 -0
- parse_bench/inference/providers/parse/kdl_frontier_nano.py +3327 -0
- parse_bench/inference/providers/parse/landingai.py +452 -0
- parse_bench/inference/providers/parse/liteparse.py +350 -0
- parse_bench/inference/providers/parse/llamaparse.py +677 -0
- parse_bench/inference/providers/parse/llamaparse_v2_normalization.py +1013 -0
- parse_bench/inference/providers/parse/markitdown.py +138 -0
- parse_bench/inference/providers/parse/mineru25.py +405 -0
- parse_bench/inference/providers/parse/mineru2605pro.py +432 -0
- parse_bench/inference/providers/parse/mineru_diffusion.py +371 -0
- parse_bench/inference/providers/parse/mistral_ocr.py +546 -0
- parse_bench/inference/providers/parse/nemotron_omni.py +473 -0
- parse_bench/inference/providers/parse/oi_parser.py +222 -0
- parse_bench/inference/providers/parse/openai.py +740 -0
- parse_bench/inference/providers/parse/opendataloader.py +152 -0
- parse_bench/inference/providers/parse/paddleocr.py +624 -0
- parse_bench/inference/providers/parse/pdf_inspector.py +142 -0
- parse_bench/inference/providers/parse/pulse.py +785 -0
- parse_bench/inference/providers/parse/pymupdf.py +207 -0
- parse_bench/inference/providers/parse/pymupdf4llm.py +356 -0
- parse_bench/inference/providers/parse/pypdf.py +179 -0
- parse_bench/inference/providers/parse/qwen.py +678 -0
- parse_bench/inference/providers/parse/rakedoc_nano.py +70 -0
- parse_bench/inference/providers/parse/reducto.py +546 -0
- parse_bench/inference/providers/parse/surya2.py +372 -0
- parse_bench/inference/providers/parse/tesseract.py +301 -0
- parse_bench/inference/providers/parse/textract.py +694 -0
- parse_bench/inference/providers/parse/unlimitedocr.py +346 -0
- parse_bench/inference/providers/parse/unstructured.py +485 -0
- parse_bench/inference/providers/parse/warp_ingest.py +199 -0
- parse_bench/inference/providers/registry.py +49 -0
- parse_bench/inference/renormalize.py +170 -0
- parse_bench/inference/runner.py +2023 -0
- parse_bench/layout_label_mapping.py +424 -0
- parse_bench/layout_projection.py +179 -0
- parse_bench/pipeline/__init__.py +1 -0
- parse_bench/pipeline/cli.py +549 -0
- parse_bench/schemas/__init__.py +33 -0
- parse_bench/schemas/evaluation.py +93 -0
- parse_bench/schemas/extract_output.py +36 -0
- parse_bench/schemas/layout_detection_output.py +545 -0
- parse_bench/schemas/layout_ontology.py +315 -0
- parse_bench/schemas/metrics.py +69 -0
- parse_bench/schemas/parse_output.py +152 -0
- parse_bench/schemas/pipeline.py +22 -0
- parse_bench/schemas/pipeline_io.py +106 -0
- parse_bench/schemas/product.py +97 -0
- parse_bench/test_cases/__init__.py +25 -0
- parse_bench/test_cases/bbox_value_strict_comparator.py +880 -0
- parse_bench/test_cases/extract_field_paths.py +164 -0
- parse_bench/test_cases/layout_attribution_generation.py +287 -0
- parse_bench/test_cases/loader.py +652 -0
- parse_bench/test_cases/parse_rule_schemas.py +1071 -0
- parse_bench/test_cases/rule_filters.py +32 -0
- parse_bench/test_cases/rule_ids.py +107 -0
- parse_bench/test_cases/schema.py +427 -0
- parse_bench/utils/__init__.py +15 -0
- parse_bench/utils/gemini_layout_utils.py +670 -0
- parse_bench/utils/text_aggregation.py +100 -0
- parse_bench-1.0.0.dist-info/METADATA +476 -0
- parse_bench-1.0.0.dist-info/RECORD +227 -0
- parse_bench-1.0.0.dist-info/WHEEL +4 -0
- parse_bench-1.0.0.dist-info/entry_points.txt +2 -0
- parse_bench-1.0.0.dist-info/licenses/LICENSE +201 -0
parse_bench/__init__.py
ADDED
|
@@ -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
|
+
)
|