Add report rebuild mode and improve histogram buckets

This commit is contained in:
kim
2026-04-02 21:37:32 +08:00
parent ee002eb1a9
commit f243664acf
7 changed files with 256 additions and 6 deletions
+24
View File
@@ -121,6 +121,30 @@ def test_build_summary_uses_adaptive_buckets_for_compact_ranges() -> None:
]
def test_build_summary_uses_finer_buckets_for_large_sample_sets_with_outliers() -> None:
records = [
_build_record(
f"clip-{index}.mp4",
size_mb=80 + (index % 8) * 60,
duration_seconds=300 + (index % 9) * 300,
)
for index in range(1_499)
]
records.append(
_build_record(
"feature-film.mkv",
size_mb=6 * 1024,
duration_seconds=6 * 3600,
codec="hevc",
)
)
summary = build_summary(records)
assert summary.duration_histogram[0].label == "0-15 min"
assert summary.size_histogram[0].label == "0-500 MB"
def test_build_summary_tracks_successful_and_failed_files() -> None:
records = [
VideoMetadata(
+62
View File
@@ -166,3 +166,65 @@ def test_main_reports_scan_progress_to_stderr(tmp_path: Path, capsys) -> None:
assert "已发现 2 个视频文件" in captured.err
assert "2/2" in captured.err
assert "02-second.mp4" in captured.err
def test_main_rebuilds_html_from_existing_report_json_without_running_probe(tmp_path: Path) -> None:
output_dir = tmp_path / "report"
output_dir.mkdir()
report_json = output_dir / "report.json"
report_json.write_text(
json.dumps(
{
"generated_at": "2026-04-02T12:00:00",
"scanned_inputs": ["videos"],
"tool_versions": {"ffprobe": "cached ffprobe", "ffmpeg": "cached ffmpeg"},
"summary": {
"total_files": 500,
"successful_files": 500,
"failed_files": 0,
"total_size_bytes": 0,
"total_duration_seconds": 0,
"duration_histogram": [{"label": "0-1 h", "count": 500}],
"size_histogram": [{"label": "0-1 GB", "count": 500}],
"video_codecs": [],
"resolutions": [],
},
"files": [
{
"path": "videos/sample.mp4",
"size_bytes": 60 * 1024 * 1024,
"duration_seconds": 120.0,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"bit_rate": 1024000,
"video_codec": "h264",
"audio_codec": "aac",
"width": 1280,
"height": 720,
"frame_rate": 25.0,
"video_stream_count": 1,
"audio_stream_count": 1,
"subtitle_stream_count": 0,
}
],
"failures": [],
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
def forbidden_runner(command: list[str]) -> str:
raise AssertionError(f"should not invoke external command: {command}")
exit_code = main(
["--report-json", str(report_json)],
command_runner=forbidden_runner,
)
assert exit_code == 0
payload = json.loads(report_json.read_text(encoding="utf-8"))
html_text = (output_dir / "report.html").read_text(encoding="utf-8")
assert payload["summary"]["duration_histogram"] == [{"label": "0-2 min", "count": 1}]
assert payload["summary"]["size_histogram"] == [{"label": "0-100 MB", "count": 1}]
assert "0-2 min" in html_text
+62 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from video_metadata_analyzer.analysis import build_summary
from video_metadata_analyzer.models import ProbeFailure, VideoMetadata
from video_metadata_analyzer.reporting import write_report_bundle
from video_metadata_analyzer.reporting import rebuild_report_bundle_from_json, write_report_bundle
def test_write_report_bundle_creates_json_csv_and_html(tmp_path: Path) -> None:
@@ -70,3 +70,64 @@ def test_write_report_bundle_creates_json_csv_and_html(tmp_path: Path) -> None:
assert "grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));" in html_text
assert "font-size: 16px;" in html_text
assert "font-size: 18px;" in html_text
def test_rebuild_report_bundle_from_json_recomputes_summary_and_html(tmp_path: Path) -> None:
report_dir = tmp_path / "report"
report_dir.mkdir()
report_json = report_dir / "report.json"
payload = {
"generated_at": "2026-04-02T12:00:00",
"scanned_inputs": ["videos"],
"tool_versions": {"ffprobe": "ffprobe version x", "ffmpeg": "ffmpeg version y"},
"summary": {
"total_files": 999,
"successful_files": 999,
"failed_files": 0,
"total_size_bytes": 0,
"total_duration_seconds": 0,
"duration_histogram": [{"label": "0-1 h", "count": 999}],
"size_histogram": [{"label": "0-1 GB", "count": 999}],
"video_codecs": [],
"resolutions": [],
},
"files": [
{
"path": "videos/a.mp4",
"size_bytes": 60 * 1024 * 1024,
"duration_seconds": 120.0,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"bit_rate": 1500000,
"video_codec": "h264",
"audio_codec": "aac",
"width": 1920,
"height": 1080,
"frame_rate": 30.0,
"video_stream_count": 1,
"audio_stream_count": 1,
"subtitle_stream_count": 0,
}
],
"failures": [
{
"path": "videos/broken.mp4",
"error_type": "CalledProcessError",
"error_message": "moov atom not found",
}
],
}
report_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
paths = rebuild_report_bundle_from_json(report_json)
rebuilt_payload = json.loads(paths["json"].read_text(encoding="utf-8"))
rebuilt_html = paths["html"].read_text(encoding="utf-8")
assert rebuilt_payload["summary"]["total_files"] == 2
assert rebuilt_payload["summary"]["successful_files"] == 1
assert rebuilt_payload["summary"]["failed_files"] == 1
assert rebuilt_payload["summary"]["duration_histogram"] == [{"label": "0-2 min", "count": 1}]
assert rebuilt_payload["summary"]["size_histogram"] == [{"label": "0-100 MB", "count": 1}]
assert "视频元数据分析报告" in rebuilt_html
assert "0-2 min" in rebuilt_html
assert "0-1 h" not in rebuilt_html