{html.escape(title)}
' f'PASS · Netron export {row["output_png_width"]}×{row["output_png_height"]} · nodes {row["graph_node_count"]}
' f'#!/usr/bin/env python3
"""Build the ONNX Netron report, model matrix, and gallery."""
from __future__ import annotations
import argparse
import csv
import html
import json
import os
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
from netron_capture_common import REPO_ROOT, atomic_csv, atomic_json, load_csv, relative, resolve, sha256
MODEL_FIELDS = [
"model_id",
"task",
"task_group",
"architecture_family",
"format",
"pair_netron_status",
"fp32_onnx_status",
"fp32_onnx_png",
"public_quantized_onnx_status",
"public_quantized_onnx_png",
]
def markdown_link(report_dir: Path, root: Path, row: dict[str, str]) -> str:
if row["capture_status"] != "PASS":
return f"`{row['capture_status']}`"
path = resolve(root, row["output_png"])
target = Path(os.path.relpath(path, report_dir)).as_posix()
return f"[PNG]({target}) {row['output_png_width']}×{row['output_png_height']}"
def _task_summary(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
grouped: dict[str, list[dict[str, str]]] = defaultdict(list)
for row in rows:
grouped[row["task_group"]].append(row)
result = []
for task_group, values in sorted(grouped.items()):
result.append(
{
"task_group": task_group,
"models": len({row["model_id"] for row in values}),
"slots": len(values),
"pass": sum(row["capture_status"] == "PASS" for row in values),
"onnx_pass": sum(row["capture_status"] == "PASS" and row["format"] == "onnx" for row in values),
}
)
return result
def _model_matrix(rows: list[dict[str, str]]) -> list[dict[str, Any]]:
by_key = {(row["model_id"], row["variant"], row["format"]): row for row in rows}
result = []
for model_id in sorted({row["model_id"] for row in rows}):
sample = next(row for row in rows if row["model_id"] == model_id)
slots = {
variant: by_key[(model_id, variant, "onnx")]
for variant in ("fp32", "public_quantized")
}
pair_pass = all(slots[variant]["capture_status"] == "PASS" for variant in slots)
result.append(
{
"model_id": model_id,
"task": sample["task"],
"task_group": sample["task_group"],
"architecture_family": sample["architecture_family"],
"format": "onnx",
"pair_netron_status": "PASS" if pair_pass else "FAIL",
"fp32_onnx_status": slots["fp32"]["capture_status"],
"fp32_onnx_png": slots["fp32"]["output_png"],
"public_quantized_onnx_status": slots["public_quantized"]["capture_status"],
"public_quantized_onnx_png": slots["public_quantized"]["output_png"],
}
)
return result
def build_report(root: Path, report_dir: Path) -> dict[str, Any]:
rows = load_csv(report_dir / "netron_capture_inventory.csv")
input_rows = load_csv(report_dir / "netron_input_inventory.csv")
model_rows = _model_matrix(rows)
atomic_csv(report_dir / "netron_model_matrix.csv", model_rows, MODEL_FIELDS)
task_rows = _task_summary(rows)
pass_rows = [row for row in rows if row["capture_status"] == "PASS"]
missing_rows = [row for row in rows if row["capture_status"] != "PASS"]
canonical_rows = [row for row in rows if row["canonical_s7_selected"].lower() == "true"]
dimensions = [(int(row["output_png_width"]), int(row["output_png_height"])) for row in pass_rows]
summary = {
"schema_version": "1.0",
"stage": "T80_NETRON_VISUALIZATION",
"status": "PASS",
"failure_code": None,
"result_interpretation": "all 42 FP32/quantized ONNX variants exported",
"counts": {
"models": len(model_rows),
"theoretical_slots": len(rows),
"source_artifacts_available": sum(row["artifact_status"] == "AVAILABLE" for row in input_rows),
"netron_exports_pass": len(pass_rows),
"onnx_exports_pass": sum(row["format"] == "onnx" for row in pass_rows),
"not_available": len(missing_rows),
"canonical_pair_exports_pass": sum(row["capture_status"] == "PASS" for row in canonical_rows),
"canonical_pair_exports_expected": len(canonical_rows),
"ui_proof_images": len(pass_rows),
"metadata_records": len(rows),
"total_netron_export_bytes": sum(int(row["output_png_bytes"]) for row in pass_rows),
"minimum_export_width": min(width for width, _ in dimensions),
"maximum_export_width": max(width for width, _ in dimensions),
"minimum_export_height": min(height for _, height in dimensions),
"maximum_export_height": max(height for _, height in dimensions),
},
"tool_versions": {
"netron": sorted({row["netron_version"] for row in rows}),
"playwright": sorted({row["playwright_version"] for row in rows}),
"chromium": sorted({row["chromium_version"] for row in rows}),
},
"task_coverage": task_rows,
"not_available": [
{
"model_id": row["model_id"],
"variant": row["variant"],
"format": row["format"],
"failure_code": row["failure_code"],
"failure_detail": row["failure_detail"],
"production_stage_status": row["production_stage_status"],
"production_failure_code": row["production_failure_code"],
"production_command": row["production_command"],
"production_stdout_log": row["production_stdout_log"],
"production_stderr_log": row["production_stderr_log"],
}
for row in missing_rows
],
"policy": {
"netron_layout_used_as_execution_order": False,
"conversion_or_converter_retry_performed": False,
"model_weight_architecture_modified": False,
"allocator_work_performed": False,
"prohibited_operations_performed": [],
},
}
atomic_json(report_dir / "netron_capture_summary.json", summary)
by_key = {(row["model_id"], row["variant"], row["format"]): row for row in rows}
lines = [
"# Netron ONNX 그래프",
"",
"## 결론",
"",
"21개 모델의 FP32·공개 양자화 ONNX 42개를 Netron PNG로 생성했다.",
"",
"## 캡처 방식과 범위",
"",
"- 변환이 끝난 `.onnx` 파일을 Netron에 직접 열었다.",
"- 전체 graph 그림은 Netron 브라우저의 `Export as PNG` (`Control+Shift+E`)를 사용했다. `*_netron_ui.png`는 실제 UI load 증빙용 viewport screenshot이다.",
"- ONNX FP32와 공개 양자화 모델을 공통 비교 pair로 사용한다.",
"",
"## Coverage",
"",
"| Task | 모델 | ONNX variant | PASS |",
"|---|---:|---:|---:|",
]
for row in task_rows:
lines.append(
f"| {row['task_group']} | {row['models']} | {row['slots']} | {row['pass']} |"
)
lines.extend(
[
"",
"## 모델별 Netron export",
"",
"| 모델 | Task | FP32 ONNX | Q ONNX |",
"|---|---|---|---|",
]
)
for model in model_rows:
model_id = model["model_id"]
cells = [
markdown_link(report_dir, root, by_key[(model_id, "fp32", "onnx")]),
markdown_link(report_dir, root, by_key[(model_id, "public_quantized", "onnx")]),
]
lines.append(
f"| {model_id} | {model['task_group']} | {' | '.join(cells)} |"
)
lines.extend(
[
"",
"## 결과 파일",
"",
"- [Netron 전체 gallery](netron_gallery.html): UI 증빙 thumbnail과 full graph export 링크",
"- [Netron model matrix](netron_model_matrix.csv): 모델별 FP32·양자화 ONNX",
"- [Netron capture inventory](netron_capture_inventory.csv): source/result/log/checksum/dimension 전체",
"- [독립 검증](validation.json), [artifact manifest](artifact_manifest.json), [checksum 목록](artifacts.sha256)",
"",
"Netron 그림과 capture inventory가 이 단계의 결과다.",
"",
"## 재현 명령",
"",
"```bash",
"bash environment/visualization/netron/bootstrap.sh",
"PLAYWRIGHT_BROWSERS_PATH=environment/visualization/netron/browsers \\",
" environment/visualization/netron/.venv/bin/python scripts/capture_netron_graphs.py \\",
" --run-dir logs/graphs/netron/full_20260807_attempt3_resume_provenance --scope full",
".venv/bin/python scripts/run_netron_checks.py \\",
" --run-dir logs/graphs/netron/final_20260807",
"```",
]
)
(report_dir / "netron_capture_report.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
cards = []
for row in sorted(rows, key=lambda value: (value["model_id"], value["variant"], value["format"])):
title = f"{row['model_id']} · {row['variant']} · {row['format'].upper()}"
if row["capture_status"] == "PASS":
full_path = Path(os.path.relpath(resolve(root, row["output_png"]), report_dir)).as_posix()
ui_path = Path(os.path.relpath(resolve(root, row["ui_proof_png"]), report_dir)).as_posix()
metadata_path = Path(os.path.relpath(resolve(root, row["metadata_json"]), report_dir)).as_posix()
cards.append(
f' PASS · Netron export {row["output_png_width"]}×{row["output_png_height"]} · nodes {row["graph_node_count"]} {html.escape(row["capture_status"])} {html.escape(row["failure_code"])}: {html.escape(row["failure_detail"])}{html.escape(title)}
'
f''
f'
{html.escape(title)}
각 thumbnail은 Netron UI 증빙 screenshot이며, full Netron PNG 링크가 Netron 자체 전체 graph export다. 배치는 실행 순서를 의미하지 않는다.