Korean Windows fixes, and per-range grade/crop/speed + capture audio on the EDL - #157
beyondwork5 wants to merge 7 commits into
Conversation
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>
There was a problem hiding this comment.
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
| seg_audio = r.get("audio", edl_audio) | ||
| seg_crop = r.get("crop", edl_crop) | ||
|
|
||
| speed = float(r.get("speed", 1.0)) |
There was a problem hiding this comment.
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>
| 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") |
| # 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( |
There was a problem hiding this comment.
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>
| 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: |
There was a problem hiding this comment.
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>
| 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 |
| 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}") |
There was a problem hiding this comment.
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>
| seg_audio = r.get("audio", edl_audio) | ||
| seg_crop = r.get("crop", edl_crop) | ||
|
|
||
| speed = float(r.get("speed", 1.0)) |
There was a problem hiding this comment.
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>
| 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. |
There was a problem hiding this comment.
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>
| 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. |
There was a problem hiding this comment.
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>
| `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: |
There was a problem hiding this comment.
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>
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
subprocess.run(..., text=True)decodes with the locale codec (cp949 here) while ffmpeg echoes the input path to stderr, so a path containing Hangul raisedUnicodeDecodeError,stderrcame backNone, and the failure surfaced as an unrelatedTypeError. 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.timeline_viewrendered as tofu on Windows.clips_*/_ranges.jsonlets a consumer check before trusting them.New EDL fields
gradeper 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) withsignalstatson each source and correct toward the reference.cropper 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.audioat the EDL level for a fixed capture chain (highpass, EQ, gate, compressor). Deliberately not the place forloudnorm— 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.speedper 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.widthon the EDL raises the output target above the 1080p default, so a 4K source can stay full size. The default is unchanged, and--previewstays 1080p on purpose: it is a fast QC pass, not a resolution preview.One behaviour change worth calling out:
-tnow precedes-i, so the duration limits how much input is read rather than how much output is written. With-ssbefore-ithat is the fast-seek form.SKILL.mddocuments 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 outputwidth.Bug Fixes
errors="replace"instead of the locale codec, so Hangul paths no longer surface asUnicodeDecodeErrorand hide the real failure.clips_*folder gets a_ranges.jsonso consumers can tell whether the render came from the current EDL before trusting measured durations.New Features
gradeandcrop; crop runs before scale, where its values are measured.audiosets a fixed per-EDL or per-range capture chain (highpass, EQ, gate, compressor);loudnormis intentionally not part of it because per-segment normalization would flatten relative levels.speedper range time-remaps via setpts/atempo, preserving pitch; fades are timed against the post-speed duration.widthraises the output target above 1080p;--previewstays 1080p for fast QC.-tnow precedes-i, so the duration limits input read (fast-seek) instead of output write.Written for commit f46df16. Summary will update on new commits.