1仕組みと動作原理
単一のPythonスクリプトでffmpeg/ffprobeを制御する実装方式:ffprobeでメタデータを取得 → ffmpegでPSNR/SSIMを計算 → ffmpegで指定間隔ごとにフレームを抽出・リサイズ → フレームと指標をHTMLテンプレートに埋め込み → レポートをディスクへ出力。モデルは介在せず、完全に決定論的なパイプラインです。
for tool in ['ffmpeg', 'ffprobe']:
レポート生成は「テンプレートをレンダリングする」方式ではなく、「テンプレートを読み込み、正規表現でプレースホルダーをブロックごとに置換する」方式です。そのためテンプレート構造と密結合しており、同一段落内の改行や要素位置の変更によってサイレントに不整合が発生します。
r'<div class="metric-value">[\d.]+ dB</div>\s*<div class="metric-subtitle">偏低</div>',
フレーム画像はHTML内にインライン埋め込みされるのではなく、HTMLと同じ階層のサブディレクトリ(original/およびwechat/)にコピーされ、テンプレートから相対パスで参照されます。つまり成果物は単一ファイルではなくファイル群となります。
Copy frames to a subdirectory next to the HTML output. Args: frames: List of (timestamp, frame_path) tuples output_html_path: Path to the output HTML file subfolder: Subdirectory name (e.g., 'original', 'wechat')
入力バリデーションを徹底:パスをresolveした後に存在確認/ファイル判定/拡張子ホワイトリスト/読み込み権限/サイズ上限を検証し、ffmpegとffprobeの存在も個別に事前検証して各OS向けのインストール手順を提示します。
# Configuration constants ALLOWED_EXTENSIONS = {'.mp4', '.mov', '.avi', '.mkv', '.webm'} MAX_FILE_SIZE_MB = 500 FFMPEG_TIMEOUT = 300 # 5 minutes FFPROBE_TIMEOUT = 30 # 30 seconds BASE_FRAME_HEIGHT = 800 FRAME_INTERVAL = 5 # seconds
コマンドインジェクション対策は文字列のエスケープではなく、「引数リスト + shell=True不使用 + タイムアウト」の組み合わせに依存しており、subprocessの適切な利用法を踏襲しています。
try: result = subprocess.run( args, capture_output=True, timeout=timeout, check=True, text=True ) return result.stdout
指標を計算する前に「2つの動画が同一内容か」の整合性検証(再生時間差の閾値判定、圧縮後に容量が増加した場合の警告)を実施し、異なる動画同士の比較を品質分析結果として出力する事態を防ぎます。
""" Validate that two videos are likely the same content. Args: metadata1: First video metadata (original) metadata2: Second video metadata (compressed) duration_threshold: Maximum allowed duration difference in seconds allow_size_increase: If False, warn when compressed is larger
2コア機能
3外部依存関係
| Kind | Dependency / Endpoint |
|---|---|
| cli | ffmpeg(必需;抽帧、缩放、算 PSNR/SSIM) |
| cli | ffprobe(必需;元数据提取) |
| cli | python3(3.8+,脚本运行时;仅标准库) |
| package | img-comparison-slider(Web Component,报告页从 CDN 加载,非随包分发) |
| network | unpkg(第三方 CDN;打开报告时由浏览器发起请求) |
| network | ffmpeg 官网(文档里的安装指引链接,非运行时依赖) |
4推奨統制措置と判定根拠
- scripts/: ローカルスクリプトまたはコマンドの実行: python3 scripts/compare.py <original> <compressed> [-o out.html] [--interva
- 网络端点: 外部ネットワークアクセスの宣言: 生成されたレポートページはブラウザ経由で https://unpkg.com/img-comparison-slider@8/dist/{styles.css,in を読み込みます
- 本地工作目录: ファイル読み書きおよび成果物の出力: -o で指定されたパス(デフォルトは ./comparison.html)にHTMLレポートを出力:open(output_path, 'w', encoding='
5二重目の独立検証
- [discrepancy] SKILL.md 的『frames 以 base64 内嵌进 self-contained HTML』声明是否成立 — 不成立,是本次侦查发现的最重问题。SKILL.md 第 99 行:'The script extracts frames at specified intervals (default: 5 seconds), scales them to consistent height (800px) for comparison, and embeds them as base64 data URLs in self-contained HTML. Temporary files are automatically cleaned after processing.' 实际实现是把帧复制到 HTML 同级子目录:copy_frames_to_output 的 docstring 即 'Copy frames to a subdirectory next to the HTML output.',dest_name = f"frame_{i:03d}.png"、shutil.copy2(frame_path, dest_path),调用处为 copy_frames_to_output(original_frames, output_path, 'original') 与 (compressed_frames, output_path, 'wechat')。全目录 grep base64/b64encode/data:image 在 .py 与 .html 中零命中(仅出现在 README 的依赖清单与 configuration.md 的说明文字里)。模板侧确认用相对路径:<img slot="first" id="originalImage" src="original/frame_001.png" ...>。即产物是「HTML + original/ + wechat/」一整个目录,不是单文件。
- [discrepancy] SKILL.md 的『Self-contained format (no server required, works offline)』是否成立 — 不成立(两条独立原因)。SKILL.md 第 108 行原文:'- Self-contained format (no server required, works offline)'。①存在外部 CDN 依赖:template.html 第 7-8 行加载 https://unpkg.com/img-comparison-slider@8/dist/styles.css 与 .../index.js,断网时滑块组件不可用;②依赖同级帧目录(见上一条),单独拷走 HTML 会丢帧。值得注意的是 README 自己承认了 CDN 依赖(依赖清单写 'img-comparison-slider (loaded from CDN)'),却同时在架构说明里声称 template.html 具备 'Base64-encoded image embedding'——即 README 内部亦不自洽,且与 SKILL.md 的 offline 声明互相矛盾。
- [discrepancy] README 的『Architecture』信息是否与文件系统/实现一致 — 三处不符:①行数——README 写 'compare.py # Main comparison script (696 lines)',实测 wc -l = 1036 行;②架构清单只列 video_metrics.md 与 ffmpeg_commands.md 两份 reference,漏掉实际存在且被 SKILL.md 明确引用的 references/configuration.md;③把 template.html 描述为 'Base64-encoded image embedding',与实现不符(见第 1 条)。
- [discrepancy] 文档是否宣称了未实现的能力(VMAF 等) — SKILL.md 层面是对的:它只说两个指标(PSNR、SSIM),与脚本一致(grep -i vmaf 在 compare.py 中零命中)。但 references/video_metrics.md(4 处 VMAF)与 references/ffmpeg_commands.md(3 处,含 'VMAF Calculation' 小节)都提供了 VMAF 的定义与 ffmpeg 命令。即参考材料覆盖了脚本未实现的第三指标——不是夸大声明(SKILL.md 未据此承诺),而是「参考材料超出工具能力」,使用者若按 reference 期待 VMAF 输出会落空。
- [ok] 三条安全声明(path validation / command injection prevention / resource limits)是否有代码对应 — 逐条核实成立:①path validation —— validate_video_file 内 Path(path).resolve()、exists()、is_file()、后缀白名单 ALLOWED_EXTENSIONS、os.access(R_OK)、体积上限 MAX_FILE_SIZE_MB=500;②command injection prevention —— run_ffmpeg_command 以参数列表调用 subprocess.run(docstring:'args: Command arguments as list (prevents shell injection)'),全文无 shell=True;③resource limits —— FFMPEG_TIMEOUT=300、FFPROBE_TIMEOUT=30,且 TimeoutExpired 有专门异常映射。声明未夸大。
- [ok] 是否存在未声明的网络/凭证行为 — 反例检索失败:compare.py 的 import 段为 argparse/json/logging/os/re/subprocess/sys/tempfile/time/pathlib/typing,无 urllib/requests/socket/http;全目录 grep token/secret/password/api_key/getenv/os.environ 零命中;无 base64 大块、无 >200 字符无空格长行。结论成立。
- [discrepancy] 模板的『通用性』呈现与内容是否相符 — SKILL.md/README 把它呈现为通用的视频压缩对比工具,但 assets/template.html 的文案与路径是微信专用的:标签为「🎬 原始视频 (AVC)」与「📱 微信视频号 (HEVC)」、替换目标里有 '<div class="metric-subtitle">微信重新编码</div>'、帧相对路径写死 'wechat/frame_001.png',且 compare.py 的复制调用也写死 subfolder='wechat'(而非 'compressed')。结论:工具可用,但默认产物带有上游使用场景的专用词汇;非微信场景需自行改模板与源码常量。
- [ok] pin commit 与 skill.path 是否与任务书表格一致 — git rev-parse HEAD = d5c4678cb5d4fd6acc9c922690df035dbd33d247,与任务书表格『库内 HEAD』逐字一致;目录位于仓库根,相对路径 video-comparer;本目录最后一次提交 878f947(2026-09-07)。GitHub API 复核 MIT / stars 1392 / pushed 2026-09-15T09:30:04Z。
6結論
f6f34839800154ed…