81 lines
2.6 KiB
Python
81 lines
2.6 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
from jinja2 import Template
|
|
|
|
from .utils import ensure_dir
|
|
|
|
REPORT_TEMPLATE = """
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>BGtopoVJ Blue Box PoC Report</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 24px auto; line-height: 1.45; }
|
|
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; font-size: 14px; }
|
|
th { background: #f3f3f3; text-align: left; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 18px; }
|
|
.card { border: 1px solid #ddd; border-radius: 12px; padding: 12px; }
|
|
img { max-width: 100%; border: 1px solid #ccc; }
|
|
code { background: #f6f6f6; padding: 2px 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>BGtopoVJ Blue Rectangle/Square PoC Report</h1>
|
|
<p>This is a weak-label mining report. Treat candidates as review targets, not truth.</p>
|
|
|
|
<h2>Candidate summary</h2>
|
|
<table>
|
|
<tr><th>Metric</th><th>Value</th></tr>
|
|
<tr><td>Total candidates</td><td>{{ total }}</td></tr>
|
|
<tr><td>Average score</td><td>{{ avg_score }}</td></tr>
|
|
<tr><td>Median score</td><td>{{ med_score }}</td></tr>
|
|
</table>
|
|
|
|
<h2>By inferred fill style</h2>
|
|
{{ style_table }}
|
|
|
|
<h2>QA overlays</h2>
|
|
<div class="grid">
|
|
{% for overlay in overlays %}
|
|
<div class="card">
|
|
<p><code>{{ overlay.name }}</code></p>
|
|
<img src="{{ overlay.rel }}" />
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def build_report(candidate_csvs: list[str | Path], overlays: list[str | Path], out_html: str | Path) -> Path:
|
|
frames = [pd.read_csv(p) for p in candidate_csvs if Path(p).exists()]
|
|
df = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
|
|
out_html = Path(out_html)
|
|
ensure_dir(out_html.parent)
|
|
style_table = "<p>No candidates.</p>"
|
|
if not df.empty:
|
|
style_table = df.groupby("fill_style").agg(count=("fill_style", "size"), avg_score=("score", "mean")).reset_index().to_html(index=False)
|
|
overlay_items = []
|
|
for ov in overlays:
|
|
ov = Path(ov)
|
|
try:
|
|
rel = ov.relative_to(out_html.parent)
|
|
except ValueError:
|
|
rel = ov
|
|
overlay_items.append({"name": ov.name, "rel": str(rel).replace("\\", "/")})
|
|
html = Template(REPORT_TEMPLATE).render(
|
|
total=0 if df.empty else len(df),
|
|
avg_score="—" if df.empty else f"{df['score'].mean():.3f}",
|
|
med_score="—" if df.empty else f"{df['score'].median():.3f}",
|
|
style_table=style_table,
|
|
overlays=overlay_items,
|
|
)
|
|
out_html.write_text(html, encoding="utf-8")
|
|
return out_html
|