Skip to content

Korean Windows fixes, and per-range grade/crop/speed + capture audio on the EDL - #157

Open
beyondwork5 wants to merge 7 commits into
browser-use:mainfrom
beyondwork5:korean-windows-and-edl-range-fields
Open

beyondwork5 wants to merge 7 commits into
browser-use:mainfrom
beyondwork5:korean-windows-and-edl-range-fields

Conversation

@beyondwork5

@beyondwork5 beyondwork5 commented Sep 7, 2026

Copy link
Copy Markdown

Seven commits from using video-use daily on Korean Windows, in two groups. Each commit stands alone and compiles on its own — happy to drop or split anything.

Bug fixes

  • Korean Windows paths. subprocess.run(..., text=True) decodes with the locale codec (cp949 here) while ffmpeg echoes the input path to stderr, so a path containing Hangul raised UnicodeDecodeError, stderr came back None, and the failure surfaced as an unrelated TypeError. The concat list, transcript read and subtitle write used the locale codec too. Separately, backslashes in a subtitles path are consumed by the filtergraph parser, so libass received the path with its separators stripped — forward slashes work on every platform.
  • ffprobe output decoded as utf-8, same root cause.
  • Hangul in timeline_view rendered as tofu on Windows.
  • A clips folder now records which EDL produced it. Segment filenames carry only an index and a source name, so a leftover render of a different EDL matches by name and hands back the wrong durations to anything that measures those files. This bit me: a 46-segment render left from an earlier EDL made a 41-segment one measure 259.9s instead of 480.0s, and subtitle placement reads those durations, so captions drifted with no error at all. clips_*/_ranges.json lets a consumer check before trusting them.

New EDL fields

  • grade per range. Sources shot in one session still drift in white balance when the camera restarts, and a single EDL-wide grade has nowhere to express that. Measure a fixed patch (a cheek, a wall) with signalstats on each source and correct toward the reference.
  • crop per range, applied before the scale. Crop values are measured against the source's own resolution; after the scale they reframe a different picture, and a 4K-measured crop does not even fit a 1080p frame.
  • audio at the EDL level for a fixed capture chain (highpass, EQ, gate, compressor). Deliberately not the place for loudnorm — segments are normalized independently, so a quiet one gets lifted to match a loud one and the relative levels between segments are destroyed. Loudness stays a single two-pass step on the finished cut.
  • speed per range via setpts/atempo, so pitch is preserved. The 30ms edge fades are computed against the post-atempo length, not the source duration, so they still land on the true out-point.
  • width on the EDL raises the output target above the 1080p default, so a 4K source can stay full size. The default is unchanged, and --preview stays 1080p on purpose: it is a fast QC pass, not a resolution preview.

One behaviour change worth calling out: -t now precedes -i, so the duration limits how much input is read rather than how much output is written. With -ss before -i that is the fast-seek form.

SKILL.md documents the new fields and four failure modes that are silent.

Rebased on current main. The earlier "read output fps from the EDL" change from my fork is dropped — #55 solved that better.


Summary by cubic

Fixes Korean Windows rendering, where Hangul paths crashed subprocess decoding and subtitle paths lost their separators, and guards against stale clips folders returning wrong durations. Adds per-range grade/crop/speed, an EDL-level audio capture chain, and an optional output width.

Bug Fixes

  • ffmpeg/ffprobe output is decoded as UTF-8 with errors="replace" instead of the locale codec, so Hangul paths no longer surface as UnicodeDecodeError and hide the real failure.
  • Subtitle paths use forward slashes, so the filtergraph parser doesn't strip backslashes on Windows.
  • Malgun Gothic is now in the font search path, so Hangul renders in the timeline view instead of tofu.
  • Each clips_* folder gets a _ranges.json so consumers can tell whether the render came from the current EDL before trusting measured durations.

New Features

  • A range can override the EDL-wide grade and crop; crop runs before scale, where its values are measured.
  • audio sets a fixed per-EDL or per-range capture chain (highpass, EQ, gate, compressor); loudnorm is intentionally not part of it because per-segment normalization would flatten relative levels.
  • speed per range time-remaps via setpts/atempo, preserving pitch; fades are timed against the post-speed duration.
  • width raises the output target above 1080p; --preview stays 1080p for fast QC.
  • -t now precedes -i, so the duration limits input read (fast-seek) instead of output write.

Written for commit f46df16. Summary will update on new commits.

Review in cubic

EyeOpener5 and others added 7 commits September 8, 2026 01:01
Prepend Malgun Gothic to the font search path. It covers Hangul and CJK,
so Korean transcripts draw as text rather than falling back to the
glyphless default face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three places where a non-ASCII path or a locale codec broke the render on a
Korean Windows install:

- subprocess.run(..., text=True) decodes with the locale codec (cp949), and
  ffmpeg echoes the input path to stderr. A path with Hangul in it raised
  UnicodeDecodeError, stderr came back None, and the failure surfaced as an
  unrelated TypeError. Read it as utf-8 with errors="replace".
- The concat list, the transcript read and the subtitle write all used the
  locale codec too. Pin them to utf-8.
- Backslashes in a subtitles path are eaten by the filtergraph parser, so
  libass received the path with its separators stripped. Use forward slashes,
  which work on every platform.
- `speed` on a range time-remaps that segment (setpts + atempo, so pitch is
  preserved). The 30ms edge fades are computed against the post-atempo
  length, not the source duration, so they still land on the true out-point.
- `width` on the EDL raises the output target above the 1080p default, so a
  4K source can stay at full size. `--preview` stays 1080p on purpose: it is
  a fast QC pass, not a resolution preview, and inheriting a 4K target would
  silently make it slow. Draft keeps its own 1280 override.
- `-t` now precedes `-i`, so the duration limits how much input is read
  rather than how much output is written. With `-ss` before `-i` this is the
  fast-seek form.
세그먼트 파일명은 seg_05_C0017.mp4 처럼 인덱스와 소스명뿐이라,
한 프로젝트에 EDL이 여러 개면 남의 렌더가 이름만으로 맞아떨어진다.

실제로 v3(46구간) 렌더가 남은 상태에서 c0017(41구간)을 재니
480.02초가 259.9초로 나왔다. 총 길이만 틀리면 경고 한 줄로 끝이지만,
같은 실측 길이를 자막 배치도 쓰기 때문에 조용히 어긋날 수 있었다.

뽑은 구간을 clips_*/_ranges.json 에 남긴다. 읽는 쪽이 대조해서
자기 EDL의 렌더가 아니면 실측 대신 계산치로 물러난다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cale

한 세션에서 찍어도 카메라를 껐다 켜면 클립마다 화이트밸런스가 틀어진다.
EDL 전체에 하나뿐인 grade 로는 그 차이를 표현할 자리가 없어서, range 가
grade 를 덮어쓸 수 있게 했다.

촬영 세팅마다 고정으로 거는 오디오 체인(highpass, EQ, 게이트, 컴프)도
EDL 에서 지정한다. loudnorm 은 여기 넣으면 안 된다 — 세그먼트를 각각
정규화하면 조용한 구간이 큰 구간 수준으로 올라와 상대 음량이 무너진다.
라우드니스는 완성본에 2-pass 로 한 번만 건다.

crop 은 scale 앞에 건다. 크롭값은 원본 해상도 기준으로 재기 때문에
스케일 뒤에 걸면 다른 그림을 자르게 된다 — 4K 기준 3400x1912 는
1920 프레임에 아예 들어가지도 않는다.

검증: 같은 20-21초 구간을 오디오 체인 유무로만 갈라 재니 80Hz 이하가
6dB 차이(-84.3 / -78.3). crop 은 4K 원본에서 프레임 해시가 달라졌고
출력은 스케일로 정규화돼 동일 규격.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
is_hdr_source / is_portrait_source 가 text=True 만 주고 인코딩을 안 정해서
로케일(한국어 Windows = cp949)로 디코드한다. 경로에 한글이 있으면 리더
스레드가 UnicodeDecodeError 로 죽는다.

죽는 것 자체보다, 그 바람에 정작 봐야 할 ffmpeg 에러가 사라지는 게 문제다.
실제로 이번에 EDL 소스 경로가 틀린 걸 추적하는데 진짜 원인 대신 이 스택
트레이스만 보였다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
per-range grade/crop 와 audio 체인을 render.py 에 넣고 EDL 포맷 문서를
안 고쳤다. 엔진이 읽는 필드와 문서가 어긋난 채로 두면 다음 사람이
없는 기능을 찾거나 있는 기능을 못 쓴다.

같이 올린 Hard Rule 넷은 전부 이번에 실제로 물린 것들이고, 공통점은
에러가 안 난다는 것이다.

13. 실측 길이를 쓰되, 그 클립이 이 EDL 것인지 확인한다
    (46세그먼트에서 계산 340.96s / 실측 344.31s. 남의 렌더가 파일명만으로
     맞아떨어져 480s 를 259.9s 로 읽은 적이 있다)
14. loudnorm 은 완성본에 한 번만. 세그먼트별로 걸면 상대 음량이 무너진다
15. crop 은 scale 앞에
16. 프레임 시퀀스는 5자리. %04d 는 8999에서 조용히 멈춘다

브랜드·사업 어휘는 넣지 않았다 — 공개 리포다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

8 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="helpers/render.py">

<violation number="1" location="helpers/render.py:306">
P2: ffmpeg's `atempo` filter only accepts tempo in [0.5, 100.0]. If an EDL sets `speed` below 0.5 (e.g. 0.25x slow-motion) or above 100, the audio filter graph fails and the whole segment extraction errors out, even though `setpts=PTS/{speed}` on the video accepts any value. Consider chaining multiple `atempo` filters for out-of-range speeds, or clamping/documenting the supported range.</violation>

<violation number="2" location="helpers/render.py:400">
P2: When a range overrides `grade` with the documented `"auto"` value, the renderer sends `__AUTO__` to ffmpeg and the extraction fails. Resolve per-range `"auto"` through `auto_grade_for_clip` just like an EDL-wide auto grade.</violation>

<violation number="3" location="helpers/render.py:409">
P2: Reject non-finite and non-positive range speeds before calculating the output duration or constructing ffmpeg filters.</violation>

<violation number="4" location="helpers/render.py:409">
P2: When a range uses `speed != 1.0`, the extracted segment's output duration becomes `duration/speed`, but `build_master_srt` still advances its caption timeline by the unadjusted source `seg_duration = end - start` (lines ~503/544) and maps caption times from source timestamps. Every segment after the sped one drifts, and captions inside the sped segment no longer match the audio. The `_ranges.json` and SRT are also consistent with source time, not post-speed output time. If `speed` is meant to be combined with `--build-subtitles`, the SRT offsets must be scaled by `1/speed` per segment (and the ranges recorded in `_ranges.json` should reflect the output duration).</violation>

<violation number="5" location="helpers/render.py:425">
P2: Include the effective rendering parameters, or an EDL/configuration hash, in `_ranges.json`; otherwise comparing this marker cannot detect stale clips from an EDL with the same source ranges but different speed or edits.</violation>
</file>

<file name="SKILL.md">

<violation number="1" location="SKILL.md:34">
P3: In hard rule 13, "each renders ~1 frame long" is missing the comparative and reads as "each segment is about one frame in length". Say "each renders ~1 frame longer than the EDL arithmetic" (and note it's ~2 frames at common fps if the number should match the 340.96 vs 344.31 example).</violation>

<violation number="2" location="SKILL.md:34">
P3: Adding hard rules 13–16 makes SKILL.md define 16 hard rules, but README.md line 110 still says "12 hard rules". Update the README count (and the "the 12 hard rules" reference) to 16 so the two documents agree.</violation>

<violation number="3" location="SKILL.md:298">
P3: The EDL section documents per-range `grade`/`crop`/`audio` but omits the other new fields shipped in this change: per-range `speed` (setpts/atempo, duration becomes `duration/speed`) and EDL-wide `width` (e.g. 3840 for 4K while preview stays 1080p). Add them so the manual matches the renderer.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread helpers/render.py
seg_audio = r.get("audio", edl_audio)
seg_crop = r.get("crop", edl_crop)

speed = float(r.get("speed", 1.0))

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Reject non-finite and non-positive range speeds before calculating the output duration or constructing ffmpeg filters.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 409:

<comment>Reject non-finite and non-positive range speeds before calculating the output duration or constructing ffmpeg filters.</comment>

<file context>
@@ -350,18 +394,39 @@ def extract_all_segments(
+        seg_audio = r.get("audio", edl_audio)
+        seg_crop = r.get("crop", edl_crop)
 
+        speed = float(r.get("speed", 1.0))
         note = r.get("beat") or r.get("note") or ""
-        print(f"  [{i:02d}] {src_name}  {start:7.2f}-{end:7.2f}  ({duration:5.2f}s)  {note}")
</file context>
Suggested change
speed = float(r.get("speed", 1.0))
speed = float(r.get("speed", 1.0))
if not 0 < speed < float("inf"):
raise ValueError("range speed must be finite and greater than zero")
Fix with cubic

Comment thread helpers/render.py
# only an index and a source name, so a leftover render of a *different* EDL
# can match by name and hand back the wrong durations to anything that
# measures these files. Consumers compare this list before trusting them.
(clips_dir / "_ranges.json").write_text(

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Include the effective rendering parameters, or an EDL/configuration hash, in _ranges.json; otherwise comparing this marker cannot detect stale clips from an EDL with the same source ranges but different speed or edits.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 425:

<comment>Include the effective rendering parameters, or an EDL/configuration hash, in `_ranges.json`; otherwise comparing this marker cannot detect stale clips from an EDL with the same source ranges but different speed or edits.</comment>

<file context>
@@ -350,18 +394,39 @@ def extract_all_segments(
+    # only an index and a source name, so a leftover render of a *different* EDL
+    # can match by name and hand back the wrong durations to anything that
+    # measures these files. Consumers compare this list before trusting them.
+    (clips_dir / "_ranges.json").write_text(
+        json.dumps([{"source": r["source"], "start": r["start"], "end": r["end"]}
+                    for r in ranges], ensure_ascii=False),
</file context>
Fix with cubic

Comment thread helpers/render.py
Comment on lines +400 to 404
if "grade" in r:
seg_filter = resolve_grade_filter(r["grade"])
elif is_auto:
seg_filter, _stats = auto_grade_for_clip(src_path, start=start, duration=duration, verbose=False)
else:

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a range overrides grade with the documented "auto" value, the renderer sends __AUTO__ to ffmpeg and the extraction fails. Resolve per-range "auto" through auto_grade_for_clip just like an EDL-wide auto grade.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 400:

<comment>When a range overrides `grade` with the documented `"auto"` value, the renderer sends `__AUTO__` to ffmpeg and the extraction fails. Resolve per-range `"auto"` through `auto_grade_for_clip` just like an EDL-wide auto grade.</comment>

<file context>
@@ -350,18 +394,39 @@ def extract_all_segments(
+        # A range may override the EDL-wide grade. Sources shot in one session can
+        # still drift in white balance (camera restart), so a per-range correction
+        # is the only place that difference can be expressed.
+        if "grade" in r:
+            seg_filter = resolve_grade_filter(r["grade"])
+        elif is_auto:
</file context>
Suggested change
if "grade" in r:
seg_filter = resolve_grade_filter(r["grade"])
elif is_auto:
seg_filter, _stats = auto_grade_for_clip(src_path, start=start, duration=duration, verbose=False)
else:
if r.get("grade") == "auto" or (is_auto and "grade" not in r):
seg_filter, _stats = auto_grade_for_clip(src_path, start=start, duration=duration, verbose=False)
elif "grade" in r:
seg_filter = resolve_grade_filter(r["grade"])
else:
seg_filter = resolved
Fix with cubic

Comment thread helpers/render.py
if audio_filter:
af_parts.append(audio_filter) # capture chain runs on the raw audio
if speed != 1.0:
af_parts.append(f"atempo={speed}")

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: ffmpeg's atempo filter only accepts tempo in [0.5, 100.0]. If an EDL sets speed below 0.5 (e.g. 0.25x slow-motion) or above 100, the audio filter graph fails and the whole segment extraction errors out, even though setpts=PTS/{speed} on the video accepts any value. Consider chaining multiple atempo filters for out-of-range speeds, or clamping/documenting the supported range.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 306:

<comment>ffmpeg's `atempo` filter only accepts tempo in [0.5, 100.0]. If an EDL sets `speed` below 0.5 (e.g. 0.25x slow-motion) or above 100, the audio filter graph fails and the whole segment extraction errors out, even though `setpts=PTS/{speed}` on the video accepts any value. Consider chaining multiple `atempo` filters for out-of-range speeds, or clamping/documenting the supported range.</comment>

<file context>
@@ -257,19 +278,35 @@ def extract_segment(
+    if audio_filter:
+        af_parts.append(audio_filter)      # capture chain runs on the raw audio
+    if speed != 1.0:
+        af_parts.append(f"atempo={speed}")
+    af_parts.append("afade=t=in:st=0:d=0.03")
+    af_parts.append(f"afade=t=out:st={fade_out_start:.3f}:d=0.03")
</file context>
Fix with cubic

Comment thread helpers/render.py
seg_audio = r.get("audio", edl_audio)
seg_crop = r.get("crop", edl_crop)

speed = float(r.get("speed", 1.0))

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a range uses speed != 1.0, the extracted segment's output duration becomes duration/speed, but build_master_srt still advances its caption timeline by the unadjusted source seg_duration = end - start (lines ~503/544) and maps caption times from source timestamps. Every segment after the sped one drifts, and captions inside the sped segment no longer match the audio. The _ranges.json and SRT are also consistent with source time, not post-speed output time. If speed is meant to be combined with --build-subtitles, the SRT offsets must be scaled by 1/speed per segment (and the ranges recorded in _ranges.json should reflect the output duration).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 409:

<comment>When a range uses `speed != 1.0`, the extracted segment's output duration becomes `duration/speed`, but `build_master_srt` still advances its caption timeline by the unadjusted source `seg_duration = end - start` (lines ~503/544) and maps caption times from source timestamps. Every segment after the sped one drifts, and captions inside the sped segment no longer match the audio. The `_ranges.json` and SRT are also consistent with source time, not post-speed output time. If `speed` is meant to be combined with `--build-subtitles`, the SRT offsets must be scaled by `1/speed` per segment (and the ranges recorded in `_ranges.json` should reflect the output duration).</comment>

<file context>
@@ -350,18 +394,39 @@ def extract_all_segments(
+        seg_audio = r.get("audio", edl_audio)
+        seg_crop = r.get("crop", edl_crop)
 
+        speed = float(r.get("speed", 1.0))
         note = r.get("beat") or r.get("note") or ""
-        print(f"  [{i:02d}] {src_name}  {start:7.2f}-{end:7.2f}  ({duration:5.2f}s)  {note}")
</file context>
Fix with cubic

Comment thread SKILL.md
10. **Parallel sub-agents for multiple animations.** Never sequential. Spawn N at once via the `Agent` tool; total wall time ≈ slowest one.
11. **Strategy confirmation before execution.** Never touch the cut until the user has approved the plain-English plan.
12. **All session outputs in `<videos_dir>/edit/`.** Never write inside the `video-use/` project directory.
13. **Human-facing times come from measured segment durations, not EDL arithmetic.** Segments are cut to frame boundaries, so each renders ~1 frame long; over 46 segments that was 340.96s of arithmetic against 344.31s on disk. Subtitle and overlay placement must read the rendered files. And **a clips directory belongs to exactly one EDL** — segment filenames carry only an index and a source name, so a leftover render of a different EDL matches by name and hands back the wrong durations. Compare `clips_*/_ranges.json` before trusting them. Silent failure: captions drift with no error.

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: In hard rule 13, "each renders ~1 frame long" is missing the comparative and reads as "each segment is about one frame in length". Say "each renders ~1 frame longer than the EDL arithmetic" (and note it's ~2 frames at common fps if the number should match the 340.96 vs 344.31 example).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At SKILL.md, line 34:

<comment>In hard rule 13, "each renders ~1 frame long" is missing the comparative and reads as "each segment is about one frame in length". Say "each renders ~1 frame longer than the EDL arithmetic" (and note it's ~2 frames at common fps if the number should match the 340.96 vs 344.31 example).</comment>

<file context>
@@ -31,6 +31,10 @@ These are the things where deviation produces silent failures or broken output.
 10. **Parallel sub-agents for multiple animations.** Never sequential. Spawn N at once via the `Agent` tool; total wall time ≈ slowest one.
 11. **Strategy confirmation before execution.** Never touch the cut until the user has approved the plain-English plan.
 12. **All session outputs in `<videos_dir>/edit/`.** Never write inside the `video-use/` project directory.
+13. **Human-facing times come from measured segment durations, not EDL arithmetic.** Segments are cut to frame boundaries, so each renders ~1 frame long; over 46 segments that was 340.96s of arithmetic against 344.31s on disk. Subtitle and overlay placement must read the rendered files. And **a clips directory belongs to exactly one EDL** — segment filenames carry only an index and a source name, so a leftover render of a different EDL matches by name and hands back the wrong durations. Compare `clips_*/_ranges.json` before trusting them. Silent failure: captions drift with no error.
+14. **`loudnorm` runs once, on the finished cut.** Never inside a per-segment capture chain — segments are normalized independently, so a quiet one gets lifted to match a loud one and the relative levels between segments are destroyed. Silent failure: the mix sounds flat and pumped.
+15. **`crop` before `scale`.** Crop values are measured against the source's own resolution; after the scale they reframe a different picture, and a 4K-measured crop does not even fit a 1080p frame.
</file context>
Fix with cubic

Comment thread SKILL.md
10. **Parallel sub-agents for multiple animations.** Never sequential. Spawn N at once via the `Agent` tool; total wall time ≈ slowest one.
11. **Strategy confirmation before execution.** Never touch the cut until the user has approved the plain-English plan.
12. **All session outputs in `<videos_dir>/edit/`.** Never write inside the `video-use/` project directory.
13. **Human-facing times come from measured segment durations, not EDL arithmetic.** Segments are cut to frame boundaries, so each renders ~1 frame long; over 46 segments that was 340.96s of arithmetic against 344.31s on disk. Subtitle and overlay placement must read the rendered files. And **a clips directory belongs to exactly one EDL** — segment filenames carry only an index and a source name, so a leftover render of a different EDL matches by name and hands back the wrong durations. Compare `clips_*/_ranges.json` before trusting them. Silent failure: captions drift with no error.

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Adding hard rules 13–16 makes SKILL.md define 16 hard rules, but README.md line 110 still says "12 hard rules". Update the README count (and the "the 12 hard rules" reference) to 16 so the two documents agree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At SKILL.md, line 34:

<comment>Adding hard rules 13–16 makes SKILL.md define 16 hard rules, but README.md line 110 still says "12 hard rules". Update the README count (and the "the 12 hard rules" reference) to 16 so the two documents agree.</comment>

<file context>
@@ -31,6 +31,10 @@ These are the things where deviation produces silent failures or broken output.
 10. **Parallel sub-agents for multiple animations.** Never sequential. Spawn N at once via the `Agent` tool; total wall time ≈ slowest one.
 11. **Strategy confirmation before execution.** Never touch the cut until the user has approved the plain-English plan.
 12. **All session outputs in `<videos_dir>/edit/`.** Never write inside the `video-use/` project directory.
+13. **Human-facing times come from measured segment durations, not EDL arithmetic.** Segments are cut to frame boundaries, so each renders ~1 frame long; over 46 segments that was 340.96s of arithmetic against 344.31s on disk. Subtitle and overlay placement must read the rendered files. And **a clips directory belongs to exactly one EDL** — segment filenames carry only an index and a source name, so a leftover render of a different EDL matches by name and hands back the wrong durations. Compare `clips_*/_ranges.json` before trusting them. Silent failure: captions drift with no error.
+14. **`loudnorm` runs once, on the finished cut.** Never inside a per-segment capture chain — segments are normalized independently, so a quiet one gets lifted to match a loud one and the relative levels between segments are destroyed. Silent failure: the mix sounds flat and pumped.
+15. **`crop` before `scale`.** Crop values are measured against the source's own resolution; after the scale they reframe a different picture, and a 4K-measured crop does not even fit a 1080p frame.
</file context>
Fix with cubic

Comment thread SKILL.md
`grade` is a preset name or raw ffmpeg filter. `overlays` are rendered animation clips. `subtitles` is optional and applied LAST.
`grade` is a preset name, a raw ffmpeg filter, or `"auto"`. `overlays` are rendered animation clips. `subtitles` is optional and applied LAST.

A **range** may carry its own `grade` and `crop`, overriding the EDL-wide value:

@cubic-dev-ai cubic-dev-ai Bot Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The EDL section documents per-range grade/crop/audio but omits the other new fields shipped in this change: per-range speed (setpts/atempo, duration becomes duration/speed) and EDL-wide width (e.g. 3840 for 4K while preview stays 1080p). Add them so the manual matches the renderer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At SKILL.md, line 298:

<comment>The EDL section documents per-range `grade`/`crop`/`audio` but omits the other new fields shipped in this change: per-range `speed` (setpts/atempo, duration becomes `duration/speed`) and EDL-wide `width` (e.g. 3840 for 4K while preview stays 1080p). Add them so the manual matches the renderer.</comment>

<file context>
@@ -286,7 +293,13 @@ Match the source unless the user asked for something specific. Common targets: `
-`grade` is a preset name or raw ffmpeg filter. `overlays` are rendered animation clips. `subtitles` is optional and applied LAST.
+`grade` is a preset name, a raw ffmpeg filter, or `"auto"`. `overlays` are rendered animation clips. `subtitles` is optional and applied LAST.
+
+A **range** may carry its own `grade` and `crop`, overriding the EDL-wide value:
+
+- `grade` per range — sources shot in one session still drift in white balance when the camera restarts, and a single EDL-wide grade has nowhere to express that difference. Measure a fixed patch (a cheek, a wall) with `signalstats` on each source and correct toward the reference.
</file context>
Fix with cubic

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