Skip to content

縦型切り抜き(REFRAME): 顔追跡クロップの基準凍結+実装(葉A/B/D/D2/E/G/G2/G3/H/H2/H3) - #54

Merged
github-actions[bot] merged 7 commits into
mainfrom
claude/reframe
Aug 10, 2026
Merged

github-actions[bot] merged 7 commits into
mainfrom
claude/reframe

Conversation

@rahiseko-alt

@rahiseko-alt rahiseko-alt commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

概要

docs/roadmap.html の G-EDIT-REFRAME(縦型切り抜き)サブツリーについて、基準凍結から実装・独立検証までを行った。

  • 対象は葉 A/B/D/D2/E/G/G2/G3/H/H2/H3(C/Fは実写素材の調達=D-7がマスター判断待ちのため今回スコープ外)。
  • criteria/verify は basis-reviewer による5ラウンドの敵対的レビューを経て凍結(round1〜3は元々あったA/B/D/Eの分解不備・原子性違反を是正、round4はG2/H2がE側フォールバック(3人以上)の音声保持を検証していなかった十分性の穴を発見しG3/H3を新設、round5でverdict: no_objection)。
  • 実装は independent-verifier による独立検証済み(criteria/verify本文どおりに測っているか、対照実装が正しく不合格になるか、3種の敵対的破壊(letterbox固定/中央固定クロップ/音声map除去/チャンネル入替え)が正しく検出されるか、独立手法(ffmpeg cropdetect・rawvideo再計算)での数値一致、3回連続実行での再現性、非フィクスチャ入力でのオーバーフィット有無を確認)。

変更内容

  • docs/roadmap.html: G-EDIT-REFRAMEの基準凍結(A/B/D/E是正、D2/G/G2/G3/H/H2/H3新設、status: todo→doing)
  • video-shorts/src/reframe.py(新規): 顔検出・追跡による縦型クロップ本体。face_mosaic.pyのYuNet検出器(SCORE_THRESHOLD=0.6, NMS_THRESHOLD=0.3)とFaceTracker(hold_frames=8)を再利用し、保持後のトラック数が1〜2人ならffmpegのcropフィルタで追従クロップ、0人または3人以上ならrender-vertical.mjsと同じletterbox(scale+pad)方式へ自動切替え。音声は-map 1:a? -c:a copyで無変換コピー。
  • video-shorts/src/reframe_cli.py(新規): CLIラッパー。
  • video-shorts/tests/reframe-verify-check.py(新規): 11 leafのverify手順と対照実装を実際に実行する自動テスト(34 PASS/0 FAIL)。package.jsonのtestチェーンに登録。
  • video-shorts/tests/reframe_fixtures.py / reframe-fixtures-check.py: 検証素材(RF-R/RF-N/RF-M、音声付き版RF-R-A/RF-N-A/RF-M-A)の生成・自己検査。

テスト計画

  • pnpm --filter video-shorts test(674 PASS / 0 FAIL、うち新規34件含む)
  • node scripts/verify-roadmap-evidence.mjs
  • BASE_REF=origin/main node scripts/verify-criteria-freeze.mjs
  • independent-verifierによる独立検証(実測値の独立再計算・敵対的破壊テスト・再現性・オーバーフィット確認)
  • CI(ci-green)が緑になったら、そのrun URLを11 leafのevidenceへ記録しstatusをdoneにする(本PR後続コミットで実施)

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added automatic vertical video reframing that tracks one or two faces for dynamic cropping.
    • Uses letterboxing when face tracking is unavailable or detects more than two faces.
    • Added a command-line workflow with clear success and error reporting.
    • Preserves source audio, including stereo channels, during processing.
  • Tests

    • Added comprehensive checks for output dimensions, face framing, fallback behavior, audio integrity, and fixture accuracy.
  • Documentation

    • Updated the roadmap and acceptance criteria to reflect completed reframing work.

claude added 5 commits August 10, 2026 05:04
着手前に basis-reviewer へ2巡掛けて凍結した。

【固定素材の作り直し】旧fixtureは反証で崩れていた(RF-Rは等速直線のみ
で決め打ちパンが通る、RF-Nは移動量とim閾値の余裕が無い、RF-Mは全30コマ
完全同一画像で絵の保持を原理的に測れない)。数値で作り直し、名前も
G-EDITORの旧「素材R/N/M」(別実体・使用中)との衝突を避けRF-R/RF-N/RF-M
とした。元画像のSHA-256照合と可逆書き出しの自己検査をtests/として作り
pnpm testへ登録した。

【葉Dを分割】黒帯の存在(D)と絵の保持(D2)が1criteriaに同居していた原子性
違反を2葉に分割。

【葉Eを再設計】旧「切り抜き成功or黒帯」の二分岐は、切り抜き分岐を通ると
絵の保持を一度も測らない欠陥を持っていた。RF-Mへ動きを入れたうえで
「3人以上は常に黒帯へ落ちる」という単一の振る舞いに絞った。
「MAX_TRACKED_FACES=2」という具体的定数の凍結は、1巡目のbasis-reviewer
指摘(RF-Mが常に3人固定のためこの葉では上限の値そのものを反証できない)
を受けて見送った。

【葉G/Hを新設】凍結素材が無音だったため切り抜きが音声を落としても全葉
緑のままだった穴を塞ぐ。音声の存在(G)と左右チャンネルの正しさ(H)。

葉Bのverifyに顔検出器の設定(cv2.FaceDetectorYN, SCORE_THRESHOLD=0.6,
NMS_THRESHOLD=0.3)、葉D2のverifyに「各フレームごとに3.0以下」の判定
単位を明記した(いずれもbasis-reviewer 2巡目前の指摘)。

葉C・Fは実写素材調達(D-7)がマスター判断待ちのため引き続き対象外。

pnpm -r test 全緑。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNo27NkwpWAcFS1BkQ75B8
basis-reviewer round3指摘: 葉G/H(音声保持・左右チャンネル)がRF-R-A(追跡成功パス)と
RF-N-A(黒帯フォールバックパス)という独立して落ちうる2つの受入事実を1criteriaのAND条件に
詰め込んでおり、AGENTS.mdの原子性規律に違反していた。G→G+G2、H→H+H2へ分割し、
親detailに音声素材仕様(RF-R-A/RF-N-A/RF-M-Aの周波数・長さ)を明記した。
basis-reviewer round4指摘: G2/H2はD/D2側フォールバック(顔0人・RF-N-A)のみを
カバーしており、E側フォールバック(顔3人以上で追跡破綻・RF-M-A)は凍結済みなのに
どの葉からも参照されていなかった(十分性の穴)。G3/H3を新設して埋め、親detailの
説明も実態に合わせて訂正した。
round1〜5のbasis-reviewerで反証を潰し終え、verdict: no_objectionを得た。
status を todo→doing に変え criteria/verify を凍結し、実装着手に進む。
G-EDIT-REFRAME-A/B/D/D2/E/G/G2/G3/H/H2/H3(凍結済みcriteria/verify)を実際に満たす
実装を追加した。criteria/verify の文言自体は変更していない。

- src/reframe.py: face_mosaic.py の create_detector/FaceTracker(SCORE_THRESHOLD=0.6,
  NMS_THRESHOLD=0.3, hold_frames=8を流用)で顔を検出・追跡し、保持後のトラック数が
  1〜2人ならffmpegのcropフィルタ(フレーム番号nに応じたx位置式)で追従クロップ、
  0人または3人以上ならrender-vertical.mjsと同じscale+padフィルタ(letterbox)へ
  自動で切り替える。区間ごとに別ffmpeg呼び出しで書き出しconcatし、音声は入力から
  `-map 1:a? -c:a copy`で無変換コピーする。
- src/reframe_cli.py: `python3 src/reframe_cli.py <入力mp4> <出力mp4>` のCLIラッパー。
- tests/reframe-verify-check.py: 11 leafのverify手順(黒帯%・顔検出30/30・letterbox参照との
  平均絶対差・音声の存在/長さ・FFT左右周波数)と、各対照実装(letterbox/decoy-pan/引き伸ばし/
  中央固定クロップ/真っ黒/1コマ目固着/常に重心追従/音声を落とす/チャンネル入替え/
  モノラル混合)を実際に生成して不合格になることを、34件のPASS/FAILとして自動検証する。
  package.json の test チェーン末尾に登録した。

pnpm -r test: 全体で674 PASS / 0 FAIL(video-shorts単体の新規追加ぶんは34 PASS / 0 FAIL)。
pnpm -r --if-present typecheck/lint/build: 対象パッケージ無し。pnpm audit --audit-level
moderate: 脆弱性0件。node scripts/verify-roadmap-evidence.mjs: OK(roadmap.html未変更)。

docs/roadmap.htmlのstatus更新・pushはマスター確認後に別途行う(今回は含めない)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNo27NkwpWAcFS1BkQ75B8
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rahiseko-alt, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a17833d2-c4ea-4aee-bad5-52efc6d58543

📥 Commits

Reviewing files that changed from the base of the PR and between b87d5c3 and d6e3a1b.

📒 Files selected for processing (2)
  • docs/failures.md
  • docs/roadmap.html
📝 Walkthrough

Walkthrough

Changes

The PR adds a face-tracked vertical reframing module and CLI. It crops videos with one or two tracked faces and uses letterbox fallback otherwise. It preserves audio, adds RF fixtures with stereo audio, runs fixture and output verification checks, and updates roadmap acceptance evidence.

Vertical Reframing

Layer / File(s) Summary
Reframing fixtures and audio inputs
video-shorts/tests/reframe_fixtures.py
RF-R, RF-N, and RF-M now use revised motion and occlusion patterns. RF audio fixtures contain distinct stereo frequencies.
Face tracking and segment rendering
video-shorts/src/reframe.py
The module detects and tracks faces, builds crop or letterbox segments, renders them with ffmpeg, concatenates segments, copies input audio, and returns diagnostics.
CLI entrypoint and error handling
video-shorts/src/reframe_cli.py
The CLI validates arguments and input files, runs reframe, reports results, and returns status codes.
Fixture integrity checks
video-shorts/tests/reframe-fixtures-check.py
The script checks source hashes, lossless video frames, expected face counts, and the audio-backed fixture.
Reframing verification and acceptance evidence
video-shorts/tests/reframe-verify-check.py, video-shorts/package.json, docs/roadmap.html
Verification covers geometry, black bars, face size, fallback frame preservation, audio duration, stereo channels, and counterexamples. The package test command runs both verification scripts, and the roadmap records the results.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant reframe
  participant ffmpeg
  participant InputAudio
  CLI->>reframe: Submit input and output paths
  reframe->>ffmpeg: Render crop or letterbox segments
  reframe->>ffmpeg: Concatenate rendered segments
  InputAudio->>ffmpeg: Supply optional input audio
  ffmpeg-->>reframe: Return final video
  reframe-->>CLI: Return diagnostics and status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、顔追跡クロップの基準凍結と実装という変更の中心内容を明確に示しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/reframe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

PR #54でCI(ci-green含む全チェック)緑を確認。independent-verifierによる
独立検証(criteria/verify本文どおりの測定・敵対的破壊テスト・独立再計算・
再現性・オーバーフィット無しの確認)も完了。handoffを更新。
@rahiseko-alt
rahiseko-alt marked this pull request as ready for review August 10, 2026 06:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
video-shorts/package.json (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

テスト連結が長くなり、実行時間と切り分けが重くなります。

reframe-verify-check.py は reframe() を6回実行し、多数の ffmpeg エンコードを行います。&& の直列連結では、最後尾に追加したこのテストへ到達するまでに全ての先行テストを通す必要があります。将来、test:fast と test:media のようにスクリプトを分けると、開発時の反復が速くなります。CI では従来どおり全体を実行してください。

現在の構成は既存スタイルに沿っているため、対応は任意です。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/package.json` at line 5, Optionally split the package.json test
command into focused scripts such as test:fast and test:media, moving the
expensive reframe-verify-check.py and related media tests into the media group
while preserving a full test script that runs the complete suite for CI.
video-shorts/src/reframe_cli.py (1)

37-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

モード判定のしきい値が実装と二重定義になっています。

reframe.py は max_tracked(既定 MAX_TRACKED_FACES)で crop/letterbox を決め、その結果を info["segments"] の mode として返します。CLI 側は 1 <= c <= 2 を直接書いているため、MAX_TRACKED_FACES を変更すると表示が実際の処理と食い違います。返り値の segments から集計すると、判定が1か所に集約されます。

♻️ 提案する修正
-    modes = ["crop" if 1 <= c <= 2 else "letterbox" for c in info["modes"]]
-    n_crop = modes.count("crop")
-    n_letterbox = modes.count("letterbox")
+    n_crop = sum(end - start for start, end, mode in info["segments"] if mode == "crop")
+    n_letterbox = sum(end - start for start, end, mode in info["segments"] if mode != "crop")
+    n_frames = n_crop + n_letterbox

この場合、Line 41 の len(modes) は n_frames に置き換えてください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/src/reframe_cli.py` around lines 37 - 39, Update the
mode-counting logic in the CLI to derive crop and letterbox counts from the
`mode` values in `info["segments"]`, reusing the processing result from
`reframe.py` instead of the hard-coded `1 <= c <= 2` threshold. Replace the
related `len(modes)` usage with `n_frames` so the displayed counts use the
reported frame total.
video-shorts/tests/reframe-verify-check.py (1)

145-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

対照実装が reframe の非公開関数に依存しています。

reframe._crop_x_expr と reframe._extract_segment は非公開名です。対照実装が製品の内部関数を共有すると、その関数自体が壊れた場合に「本実装」と「対照」が同じ方向へ壊れ、対照が検出器として働きません。将来的には、公開ヘルパーとして切り出すか、対照側で独立に crop フィルタ文字列を組み立てることを検討してください。

現状は検証の粒度上許容できる範囲のため、今回の対応は任意です。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/tests/reframe-verify-check.py` around lines 145 - 170, In the
comparison implementations build_decoy_pan and build_always_centroid, remove
reliance on reframe._crop_x_expr and reframe._extract_segment by independently
constructing the crop filter expression and invoking the segment extraction
path. Keep the decoy and always-centroid behaviors unchanged while ensuring they
remain independent of reframe’s private helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@video-shorts/src/reframe.py`:
- Around line 80-90: Replace the full-list buffering in read_frames with
one-frame-at-a-time processing, moving face detection and FaceTracker updates
into the read loop used by analyze_tracks. Retain only the resulting analysis
records and input dimensions, and remove any callers’ dependence on a complete
decoded-frame list while preserving existing tracking behavior.
- Around line 139-163: The continuous crop flow using _crop_x_expr currently
creates one nested expression per frame, so long segments can exceed FFmpeg
limits. Update the segment-processing logic around _crop_x_expr to split
supported long crop segments into bounded frame chunks, rebase each chunk’s crop
x values so its expression starts at n=0, render each chunk separately, and
concatenate the chunk outputs while preserving frame order.

In `@video-shorts/tests/reframe-fixtures-check.py`:
- Around line 63-68: Update the face-count assertion in the fixture check to
require len(frames) == rf.FRAMES alongside all(c == want for c in counts).
Extend the failure detail to report the actual decoded frame count, while
preserving the existing count-set reporting.

In `@video-shorts/tests/reframe-verify-check.py`:
- Around line 96-101: Update per_frame_diffs to require equal-length frames_a
and frames_b before comparing frames, rather than allowing zip to truncate
silently; raise or assert on a frame-count mismatch so callers such as
diffs_black preserve the intended [:30] comparison and cannot pass with dropped
frames, while satisfying Ruff B905.
- Around line 377-382: Update the AUDIO_CASES verification loop around
ffprobe_audio and the streams[0]["duration"] access to read duration safely with
.get(). Treat a missing duration as a failed check while preserving the existing
duration tolerance validation and diagnostic output, so later cases continue to
report results instead of raising KeyError.
- Around line 401-412: Update channel_pcm to detect ffmpeg failures and handle
empty output consistently, then use the existing or newly added fmt_hz helper
for None-safe frequency formatting in all three diagnostic messages involving
fl/fr, fl_s/fr_s, and fl_m/fr_m. Preserve the current pass/fail conditions while
preventing formatting errors when dominant_freq returns None.

---

Nitpick comments:
In `@video-shorts/package.json`:
- Line 5: Optionally split the package.json test command into focused scripts
such as test:fast and test:media, moving the expensive reframe-verify-check.py
and related media tests into the media group while preserving a full test script
that runs the complete suite for CI.

In `@video-shorts/src/reframe_cli.py`:
- Around line 37-39: Update the mode-counting logic in the CLI to derive crop
and letterbox counts from the `mode` values in `info["segments"]`, reusing the
processing result from `reframe.py` instead of the hard-coded `1 <= c <= 2`
threshold. Replace the related `len(modes)` usage with `n_frames` so the
displayed counts use the reported frame total.

In `@video-shorts/tests/reframe-verify-check.py`:
- Around line 145-170: In the comparison implementations build_decoy_pan and
build_always_centroid, remove reliance on reframe._crop_x_expr and
reframe._extract_segment by independently constructing the crop filter
expression and invoking the segment extraction path. Keep the decoy and
always-centroid behaviors unchanged while ensuring they remain independent of
reframe’s private helpers.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b288b2e8-5f52-4a9f-9d8d-2cc8dfb4a823

📥 Commits

Reviewing files that changed from the base of the PR and between 3008864 and b87d5c3.

📒 Files selected for processing (7)
  • docs/roadmap.html
  • video-shorts/package.json
  • video-shorts/src/reframe.py
  • video-shorts/src/reframe_cli.py
  • video-shorts/tests/reframe-fixtures-check.py
  • video-shorts/tests/reframe-verify-check.py
  • video-shorts/tests/reframe_fixtures.py

Comment on lines +80 to +90
def read_frames(path: str):
"""全フレームを cv2 で読み込む(顔検出専用。書き出しは常に ffmpeg 側で行う)。"""
cap = cv2.VideoCapture(path)
frames = []
while True:
ok, f = cap.read()
if not ok:
break
frames.append(f)
cap.release()
return frames

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Analyze frames without retaining the full video.

read_frames() stores every decoded BGR frame until analyze_tracks() finishes. A 1080p frame uses about 6 MiB. A multi-minute input can consume tens of GiB and terminate the process.

Read, detect, and update FaceTracker one frame at a time. Retain only the analysis records and input dimensions.

Also applies to: 207-215

🧰 Tools
🪛 Ruff (0.16.1)

[warning] 81-81: Docstring contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 81-81: Docstring contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/src/reframe.py` around lines 80 - 90, Replace the full-list
buffering in read_frames with one-frame-at-a-time processing, moving face
detection and FaceTracker updates into the read loop used by analyze_tracks.
Retain only the resulting analysis records and input dimensions, and remove any
callers’ dependence on a complete decoded-frame list while preserving existing
tracking behavior.

Comment on lines +139 to +163
def _crop_x_expr(cx_values, in_w: int, crop_w: int) -> str:
"""区間内の各フレーム(相対フレーム番号 n=0..)のクロップ左端xを求める ffmpeg 式を作る。

ffmpeg の crop フィルタでは `n` はそのフィルタに入ってきたフレームの通し番号
(0始まり)を指す。trim で区間を切り出したあとに crop を掛けるので、n は
その区間内で 0 から数え直される。

注意(未確定・backlog): この実装は区間内のフレーム数だけ if(eq(n,k),...) を
ネストするため、区間が数千フレームを超えるような長尺のクロップ区間では
フィルタ文字列が非常に長くなる。今回のスコープ(30フレームの検証素材)では
問題にならないが、フルパイプライン統合時は区間をチャンクに分割するなどの
対策が必要になりうる(今回のスコープ外)。
"""
max_x = max(0, in_w - crop_w)
xs = []
for cx in cx_values:
x = int(round(cx - crop_w / 2.0))
x = max(0, min(max_x, x))
xs.append(x)
if len(set(xs)) == 1:
return str(xs[0])
expr = str(xs[-1])
for n in range(len(xs) - 2, -1, -1):
expr = f"if(eq(n\\,{n})\\,{xs[n]}\\,{expr})"
return expr

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python - <<'PY'
for frames in (1800, 3600, 5400):
    expr = "0"
    for n in range(frames - 2, -1, -1):
        expr = f"if(eq(n\\,{n})\\,0\\,{expr})"
    print(f"{frames} frames: -vf crop expression is {len(expr)} bytes")
PY

getconf ARG_MAX 2>/dev/null || true

Repository: rahiseko-alt/ai-editer

Length of output: 305


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Files matching reframe.py:\n'
fd -a 'reframe\.py$' . || true

file="$(fd 'reframe\.py$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  printf '\nRelevant reframe.py outline:\n'
  ast-grep outline "$file" --view compact || true
  printf '\nLines 1-280:\n'
  cat -n "$file" | sed -n '1,280p'
fi

printf '\nCrop-segment usage / crop expression construction searches:\n'
rg -n "_crop_x_expr|crop_x|crop|segment|trim|run_pipe|ffmpeg|ffprobe" "$file" || true

printf '\nRelevant occurrences across repo:\n'
rg -n "_crop_x_expr|crop_x|crop frame|segment" . -g '!work/**' -g '!output/**' || true

Repository: rahiseko-alt/ai-editer

Length of output: 40475


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo 'tests/reframe-verify-check.py crop-related lines:'
cat -n video-shorts/tests/reframe-verify-check.py | sed -n '120,190p'

echo
echo 'pipeline/mkdir/entrypoint references for reframe:'
rg -n "reframe|REFRAME|g-edit-reframe" video-shorts -g '!work/**' -g '!output/**' || true

Repository: rahiseko-alt/ai-editer

Length of output: 9520


🌐 Web query:

FFmpeg expression parser if eq nested expression depth maximum if(eq(n, ...) if(eq(n, ...)

💡 Result:

In FFmpeg's expression parser (implemented in libavutil/eval.c), the maximum depth allowed for nested expressions, including functions like eq or if, is 100 [1][2]. As of recent updates to the codebase, the parser explicitly checks the depth of the expression tree [3][4]. When constructing an expression, the parser calculates the depth of each node by taking the maximum depth of its children plus one [1][5]. If this depth exceeds the defined constant MAX_DEPTH (currently 100), the parser will reject the expression and return an error (AVERROR(EINVAL)) [3][5]. This mechanism was implemented to prevent stack exhaustion issues [4].

Citations:


🌐 Web query:

FFmpeg libavutil eval.c MAX_DEPTH if expression parser

💡 Result:

In FFmpeg's libavutil/eval.c, MAX_DEPTH is a preprocessor macro defined as 100 that serves as a safety limit for the recursive depth of the expression tree [1][2][3]. To prevent stack exhaustion during the parsing and evaluation of expressions, FFmpeg tracks the depth of the AVExpr tree [4]. Specifically: 1. The AVExpr struct includes a depth field [5][4][6]. 2. During the construction of expression nodes (via make_eval_expr) and the parsing of primary expressions (via parse_primary), the depth of the tree is calculated and compared against MAX_DEPTH [1][2][5]. 3. If the calculated depth exceeds 100, the parser will fail and return an error (AVERROR(EINVAL) or NULL) to prevent creating an excessively deep tree that could cause a stack overflow during recursive evaluation [1][2][5][4]. This mechanism was introduced to explicitly check the depth of the AVExpr tree, complementing existing checks on the parser's own stack depth [5][4].

Citations:


🌐 Web query:

FFmpeg libavutil eval.h MAX_DEPTH 100

💡 Result:

In FFmpeg, MAX_DEPTH is a preprocessor definition set to 100 within libavutil/eval.c [1][2]. It serves as an arbitrary limit designed to prevent unbounded recursion when parsing and evaluating mathematical expressions [3]. Introduced to improve security and stability, this constant restricts the maximum depth of the expression tree (AVExpr) that can be processed [4]. When the parser encounters an expression that would result in a tree depth exceeding this limit (e.g., deeply nested function calls or operations), it will trigger an error (typically returning AVERROR(EINVAL)) and fail to parse or evaluate the expression [4]. This check is applied both during the construction of the expression tree and when evaluating sub-expressions [4].

Citations:


Bound the dynamic crop expression size.

A continuous crop segment builds one nested if(eq(n,...),...) level for every frame. On supported inputs, a long crop segment can exceed FFmpeg's filter expression depth limit or the process argument limit before rendering starts.

If supported inputs can contain long crop segments, split crop segments into bounded frame chunks and rebase the crop x expression for each chunk, then concat the segment outputs.

🧰 Tools
🪛 Ruff (0.16.1)

[warning] 155-155: Value being cast to int is already an integer

Remove unnecessary int call

(RUF046)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/src/reframe.py` around lines 139 - 163, The continuous crop flow
using _crop_x_expr currently creates one nested expression per frame, so long
segments can exceed FFmpeg limits. Update the segment-processing logic around
_crop_x_expr to split supported long crop segments into bounded frame chunks,
rebase each chunk’s crop x values so its expression starts at n=0, render each
chunk separately, and concatenate the chunk outputs while preserving frame
order.

Comment on lines +63 to +68
counts = [len(detect_faces(f, det)) for f in frames]
check(
f"RF-{kind}: 全{rf.FRAMES}コマの顔検出件数が{want}件で揃っている",
all(c == want for c in counts),
f"実際={sorted(set(counts))}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Assert the expected frame count before accepting face counts.

all(c == want for c in counts) returns True for an empty list and also accepts a truncated fixture when every remaining frame has the expected count. video-shorts/tests/reframe_fixtures.py, Lines 300-310, only compares the decoded count with the generated count; it does not compare either count with rf.FRAMES. A 29-frame fixture can therefore pass while the message claims that all 30 frames passed.

Require len(frames) == rf.FRAMES in this assertion and include the actual frame count in the failure detail.

Proposed fix
         check(
             f"RF-{kind}: 全{rf.FRAMES}コマの顔検出件数が{want}件で揃っている",
-            all(c == want for c in counts),
-            f"実際={sorted(set(counts))}",
+            len(frames) == rf.FRAMES and all(c == want for c in counts),
+            f"実際のコマ数={len(frames)}, 顔件数={sorted(set(counts))}",
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
counts = [len(detect_faces(f, det)) for f in frames]
check(
f"RF-{kind}: 全{rf.FRAMES}コマの顔検出件数が{want}件で揃っている",
all(c == want for c in counts),
f"実際={sorted(set(counts))}",
)
counts = [len(detect_faces(f, det)) for f in frames]
check(
f"RF-{kind}: 全{rf.FRAMES}コマの顔検出件数が{want}件で揃っている",
len(frames) == rf.FRAMES and all(c == want for c in counts),
f"実際のコマ数={len(frames)}, 顔件数={sorted(set(counts))}",
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/tests/reframe-fixtures-check.py` around lines 63 - 68, Update
the face-count assertion in the fixture check to require len(frames) ==
rf.FRAMES alongside all(c == want for c in counts). Extend the failure detail to
report the actual decoded frame count, while preserving the existing count-set
reporting.

Comment on lines +96 to +101
def mean_abs_diff(a, b):
return float(np.abs(a.astype(np.int16) - b.astype(np.int16)).mean())


def per_frame_diffs(frames_a, frames_b):
return [mean_abs_diff(a, b) for a, b in zip(frames_a, frames_b)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

フレーム数の不一致が黙って見逃されます。

zip は短い方に合わせて打ち切ります。実装がコマを落とした場合、per_frame_diffs は残ったコマだけを比較し、D2/E は PASS のままになります。コマ数の一致自体を受入事実として測ってください。Ruff の B905 も同じ箇所を指しています。

🛡️ 提案する修正
 def per_frame_diffs(frames_a, frames_b):
+    if len(frames_a) != len(frames_b):
+        raise AssertionError(f"フレーム数が一致しません: {len(frames_a)} vs {len(frames_b)}")
-    return [mean_abs_diff(a, b) for a, b in zip(frames_a, frames_b)]
+    return [mean_abs_diff(a, b) for a, b in zip(frames_a, frames_b, strict=True)]

Line 335 の diffs_black は black_frames を [:30] に切っているため、この追加チェックでも成立します。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def mean_abs_diff(a, b):
return float(np.abs(a.astype(np.int16) - b.astype(np.int16)).mean())
def per_frame_diffs(frames_a, frames_b):
return [mean_abs_diff(a, b) for a, b in zip(frames_a, frames_b)]
def mean_abs_diff(a, b):
return float(np.abs(a.astype(np.int16) - b.astype(np.int16)).mean())
def per_frame_diffs(frames_a, frames_b):
if len(frames_a) != len(frames_b):
raise AssertionError(f"フレーム数が一致しません: {len(frames_a)} vs {len(frames_b)}")
return [mean_abs_diff(a, b) for a, b in zip(frames_a, frames_b, strict=True)]
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 101-101: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/tests/reframe-verify-check.py` around lines 96 - 101, Update
per_frame_diffs to require equal-length frames_a and frames_b before comparing
frames, rather than allowing zip to truncate silently; raise or assert on a
frame-count mismatch so callers such as diffs_black preserve the intended [:30]
comparison and cannot pass with dropped frames, while satisfying Ruff B905.

Source: Linters/SAST tools

Comment on lines +377 to +382
for leaf, kind, label in AUDIO_CASES:
out_path = p(f"{kind}-out.mp4")
streams = ffprobe_audio(out_path)
ok = len(streams) >= 1 and abs(float(streams[0]["duration"]) - 1.000) <= 0.05
dur = streams[0]["duration"] if streams else "N/A"
check(f"{leaf}({label}): 音声ストリームが存在し長さ1.000秒±0.05秒", ok, f"streams={len(streams)} duration={dur}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

duration が無いストリームで KeyError になります。

ffprobe の stream=duration は、コンテナや音声コーデックによっては出力されません。その場合 Line 380 は FAIL ではなく KeyError で異常終了し、G/G2/G3 の結果が出ません。.get() で受けて、欠落時は不合格として数えてください。

🛡️ 提案する修正
-    ok = len(streams) >= 1 and abs(float(streams[0]["duration"]) - 1.000) <= 0.05
-    dur = streams[0]["duration"] if streams else "N/A"
+    dur = streams[0].get("duration") if streams else None
+    ok = dur is not None and abs(float(dur) - 1.000) <= 0.05
+    dur = dur if dur is not None else "N/A"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for leaf, kind, label in AUDIO_CASES:
out_path = p(f"{kind}-out.mp4")
streams = ffprobe_audio(out_path)
ok = len(streams) >= 1 and abs(float(streams[0]["duration"]) - 1.000) <= 0.05
dur = streams[0]["duration"] if streams else "N/A"
check(f"{leaf}({label}): 音声ストリームが存在し長さ1.000秒±0.05秒", ok, f"streams={len(streams)} duration={dur}")
for leaf, kind, label in AUDIO_CASES:
out_path = p(f"{kind}-out.mp4")
streams = ffprobe_audio(out_path)
dur = streams[0].get("duration") if streams else None
ok = dur is not None and abs(float(dur) - 1.000) <= 0.05
dur = dur if dur is not None else "N/A"
check(f"{leaf}({label}): 音声ストリームが存在し長さ1.000秒±0.05秒", ok, f"streams={len(streams)} duration={dur}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/tests/reframe-verify-check.py` around lines 377 - 382, Update
the AUDIO_CASES verification loop around ffprobe_audio and the
streams[0]["duration"] access to read duration safely with .get(). Treat a
missing duration as a failed check while preserving the existing duration
tolerance validation and diagnostic output, so later cases continue to report
results instead of raising KeyError.

Comment on lines +401 to +412
fl = dominant_freq(left)
fr = dominant_freq(right)
ok = fl is not None and fr is not None and abs(fl - 440) <= 5 and abs(fr - 880) <= 5
check(f"{leaf}({label}): 左440Hz±5Hz・右880Hz±5Hzの両方を満たす", ok, f"左={fl:.2f}Hz 右={fr:.2f}Hz")

# 対照1: 左右チャンネルを入れ替えた実装
swapped = p(f"{kind}-swapped.mp4")
run_ffmpeg(["-i", out_path, "-c:v", "copy", "-af", "pan=stereo|c0=c1|c1=c0", "-c:a", "aac", "-b:a", "128k", swapped])
fl_s = dominant_freq(channel_pcm(swapped, 0))
fr_s = dominant_freq(channel_pcm(swapped, 1))
swap_ok = fl_s is not None and fr_s is not None and abs(fl_s - 440) <= 5 and abs(fr_s - 880) <= 5
check(f"{leaf}対照(左右入替え): 不合格になる", not swap_ok, f"左={fl_s:.2f}Hz 右={fr_s:.2f}Hz")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

支配周波数が None のとき、書式化で TypeError になります。

dominant_freq は、サンプルが0個のとき None を返します。ok の判定は None を考慮していますが、Line 404 の f"左={fl:.2f}Hz" は None を書式化できず TypeError で落ちます。同じ問題は Line 412 の fl_s/fr_s と Line 424 の fl_m/fr_m にもあります。根本原因は channel_pcm(Line 126-133)が ffmpeg の終了コードを確認せず、失敗時に空配列を返す点です。書式化を None 安全にし、channel_pcm で失敗を検出してください。

🛡️ 提案する修正
+def fmt_hz(v):
+    return "N/A" if v is None else f"{v:.2f}Hz"
 def channel_pcm(path, channel_index, sr=48000):
     args = [
         "ffmpeg", "-v", "error", "-i", path,
         "-af", f"pan=mono|c0=c{channel_index}",
         "-f", "f32le", "-ar", str(sr), "-",
     ]
-    out = subprocess.run(args, capture_output=True).stdout
-    return np.frombuffer(out, dtype=np.float32)
+    proc = subprocess.run(args, capture_output=True)
+    if proc.returncode != 0:
+        raise RuntimeError(f"音声抽出に失敗しました: {path}\n{proc.stderr[-2000:].decode(errors='replace')}")
+    return np.frombuffer(proc.stdout, dtype=np.float32)

書式化側は3か所とも fmt_hz を使ってください。

-    check(f"{leaf}({label}): 左440Hz±5Hz・右880Hz±5Hzの両方を満たす", ok, f"左={fl:.2f}Hz 右={fr:.2f}Hz")
+    check(f"{leaf}({label}): 左440Hz±5Hz・右880Hz±5Hzの両方を満たす", ok, f"左={fmt_hz(fl)} 右={fmt_hz(fr)}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@video-shorts/tests/reframe-verify-check.py` around lines 401 - 412, Update
channel_pcm to detect ffmpeg failures and handle empty output consistently, then
use the existing or newly added fmt_hz helper for None-safe frequency formatting
in all three diagnostic messages involving fl/fr, fl_s/fr_s, and fl_m/fr_m.
Preserve the current pass/fail conditions while preventing formatting errors
when dominant_freq returns None.

マスターの終了指示によりCodeRabbit指摘7件の修正エージェントをTaskStopで
中断した。未検証の変更はcommitせずgit stashへ退避(次セッションでpop後に
実測値の非回帰を確認して再開)。詳細をdocs/failures.mdに記録し、
meta.handoff/meta.nextを次セッション向けに更新した。
@github-actions
github-actions Bot merged commit ba15011 into main Aug 10, 2026
13 checks passed
@github-actions
github-actions Bot deleted the claude/reframe branch August 10, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants