Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
221 commits
Select commit Hold shift + click to select a range
949c02a
Add OpenSSF Scorecard and MCP registry manifests
cursoragent Oct 29, 2026
2c1dc56
Add join.py --copy (stream-copy join of matching clips) and cut.py --…
claude Oct 31, 2026
bd139c1
Add output_identity, check.py --expect-identity and check.py --compare
claude Oct 31, 2026
c6ae342
Add export.py --max-mb: a two-pass x264 encode that fits a size, meas…
claude Oct 31, 2026
ae26d28
Add export.py --preset copy --repair: remux a damaged file and count …
claude Oct 31, 2026
3284ceb
export.py --repair: judge the remux on timestamp warnings
claude Oct 31, 2026
762f1bc
Label the join copy, identity, compare, max-mb and repair additions 2.24
claude Oct 31, 2026
00e44c2
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Oct 31, 2026
6884481
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Oct 31, 2026
7c2560e
Address review: cover art, timeline times, copy-join guards, --remove…
claude Oct 31, 2026
921799e
Add a cut.py --remove demo; damage only video payload in the repair test
claude Oct 31, 2026
8f6d53c
join.py --copy: refuse a different stream layout and a start offset; …
claude Oct 31, 2026
41da3c6
check.py --expect-identity: grade an existing file under --dry-run too
claude Oct 31, 2026
387f8ed
join.py --copy checks starts without stream lengths; --expect-identit…
claude Oct 31, 2026
c89fac0
check.py: read the identity after --compare; note a mid-check change …
claude Oct 31, 2026
aefb438
Route the 2.24 features from SKILL.md; make the repair test open on e…
claude Oct 31, 2026
b483306
Repair test: fall back to MPEG-PS where ffprobe cannot read MPEG-TS
claude Oct 31, 2026
300f12e
Repair test: give the MPEG-PS fallback damage every ffmpeg reports
claude Oct 31, 2026
c6daf4d
Docs: multicam energy switch is a loudness proxy; why 啊 is not a defa…
claude Oct 31, 2026
34dfbc7
Drop color.py --tonemap bt2390 (never worked); bring README in line w…
claude Oct 31, 2026
ce1820a
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Oct 31, 2026
845f7e0
check.py --compare: pair only frames both files have; RGB and rotated…
claude Oct 31, 2026
d71ca47
join.py --copy: measure stream spans from packets; VP9; --codec; drop…
claude Oct 31, 2026
c0d546a
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Oct 31, 2026
de4d2f0
export.py --max-mb with gif / huge limits; --repair damage counting; …
claude Oct 31, 2026
a8c13a1
join.py --copy: refuse when a stream span cannot be measured; refuse …
claude Oct 31, 2026
b8a3428
join.py --copy: read packet spans as JSON with a dts fallback; rotati…
claude Oct 31, 2026
598947d
join.py --copy: read a long clip's packet span at its head and tail o…
claude Oct 31, 2026
af082e3
join.py --copy: seek a long clip's tail from its own start time
claude Oct 31, 2026
51d34e9
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Oct 31, 2026
cba64d4
Restore CHANGELOG.md to main after the 2.24.1 release bump
claude Oct 31, 2026
0bfedfb
3.0.0: require FFmpeg 6.1+ and Python 3.10+; apply the cut.py default…
claude Oct 31, 2026
dca4431
3.0.0 docs: work in progress
claude Oct 31, 2026
e82d25d
3.0.0 docs: work in progress
claude Oct 31, 2026
53e5b2c
3.0.0 docs: work in progress
claude Oct 31, 2026
463aa85
v3.0: tests for the new cut defaults, sampled guard on seeked TS wind…
claude Oct 31, 2026
bd27dc9
design-decisions: point stale test names at their 3.0 successors
claude Oct 31, 2026
1a17c9e
probe: report the source's start timecode (video stream, tmcd, then f…
claude Oct 31, 2026
ac19577
test_asr: pick the engine call out of the fake's calls (the 6.1 floor…
claude Oct 31, 2026
ac53771
Review: timecode minutes/seconds 00-59; ffprobe floor in doctor; git …
claude Oct 31, 2026
e8f31a0
Review: doctor marks every tool unusable below the floor; an old ffpr…
claude Oct 31, 2026
b7b1f41
Merge main (v3.0.0)
claude Oct 31, 2026
a3dae9a
ci: spread the suite over three machines per platform (run_parallel.p…
claude Oct 31, 2026
a441122
run_parallel --part: bound N by the work items; test that every shard…
claude Oct 31, 2026
2938c61
Merge main (#388)
claude Oct 31, 2026
01657c7
Failures say why: ffmpeg error reasons and hints, no orphan ffmpeg af…
claude Oct 31, 2026
ae1d114
Review: one encoder_missing entry, asr_engine_missing under error.rea…
claude Oct 31, 2026
4b4844e
Merge main (v3.1.0)
claude Oct 31, 2026
7c96aa4
Merge main (#390)
claude Oct 31, 2026
ef3372b
docs: a talking-head reframe demo from AI-generated footage, on the l…
claude Oct 31, 2026
1821226
silence --filler --transcribe: prime Whisper with the filler list; fi…
claude Oct 31, 2026
756c970
Pages: the two AI-footage demos as MP4 with sound, built with grid/gr…
claude Oct 31, 2026
f4eeca4
demos: the gallery intro names the two AI-footage demos
claude Oct 31, 2026
682b8be
docs/demos.md: keep render_project's --export-timeline line (a partia…
claude Oct 31, 2026
8039999
Pages: the reframe label and aria-label give the source's real size; …
claude Oct 31, 2026
d25d38f
Merge origin/main
claude Oct 31, 2026
9c54126
docs: SKILL.md restructured, no behavior change
claude Oct 31, 2026
865771f
docs: restore three SKILL.md rules the restructure cut; fix moved-tex…
claude Oct 31, 2026
6086d46
docs: step 5 keeps --expect-identity; highlight row and --from wording
claude Oct 31, 2026
d6c9050
Merge origin/main
claude Oct 31, 2026
16e0160
Fix #393-#397: lean builds, tofu emoji, SRT as --text, dropped audio …
claude Nov 5, 2026
0833d83
Merge remote branch history (same tree as main)
claude Nov 5, 2026
51534e1
#395: an auto caption leaves undrawable emoji out with a warning; Cod…
claude Nov 5, 2026
9ff4fc6
docs: qualify the emoji none mode and the one-audio-track gotcha
claude Nov 5, 2026
0d7bca4
Merge origin/main
claude Nov 5, 2026
d04a721
Escape line breaks and control characters from arguments in messages …
claude Nov 5, 2026
fe93dec
Merge remote branch history
claude Nov 5, 2026
d2347ef
Report audio tracks every tool leaves out; grid/scenes sheets without…
claude Nov 5, 2026
94c8b1d
CodeRabbit on #405: escape --opt=value and notes; no track report acr…
claude Nov 5, 2026
e233757
Merge origin/main
claude Nov 5, 2026
90d8a01
docs: README and the Pages docs layout carry the v3 brand
claude Nov 5, 2026
6e602c7
CHANGELOG: drop the 3.1.3 entries the merge left under Unreleased
claude Nov 5, 2026
b9de0f7
Side files and paths get the output guards (#408)
claude Nov 5, 2026
b76d9cb
Merge origin/main
claude Nov 5, 2026
6fb532f
HDR10 static metadata reaches libx265 and SVT-AV1, and its loss is me…
claude Nov 5, 2026
07a8911
Merge HDR10 work (#401)
claude Nov 5, 2026
e8f6ad7
CodeRabbit on #412: zero luminance, per-part frame read, V:0, SVT pre…
claude Nov 5, 2026
e14608c
CodeRabbit on #412: a still is not a picture source; read the frame w…
claude Nov 5, 2026
3a9dd96
Fix the audit G colour paths (#410)
claude Nov 5, 2026
65c8b04
Square pixels everywhere that scales; wrap graphics titles and overla…
claude Nov 5, 2026
4f76844
Merge origin/main
claude Nov 5, 2026
b88e62c
CHANGELOG: match main after the 3.1.4 release
claude Nov 5, 2026
5fad4ef
fix(timing): keep picture and sound on one clock in silence, sync, mu…
claude Nov 5, 2026
b494f46
render pack guard only where the folder exists; pin the pack table re…
claude Nov 5, 2026
b92f176
Merge origin/main
claude Nov 5, 2026
dc20e49
Merge #408 work
claude Nov 5, 2026
fb04bb0
Merge origin/main
claude Nov 5, 2026
317d644
CHANGELOG for #410
claude Nov 5, 2026
71705c4
test: check the MP4 header instead of calling file(1), which the Debi…
claude Nov 5, 2026
c2f6cf7
CodeRabbit on #413: guard every input and output the side files can c…
claude Nov 5, 2026
af16a43
Refuse hard-linked side files and allow a report inside a new --out (…
claude Nov 5, 2026
4af0165
Protect caption's --brand file from its side-file writes (#408)
claude Nov 5, 2026
20effb9
Pass -pattern_type only to image2 stills; never reuse a leftover temp…
claude Nov 5, 2026
fba851b
Merge branch 'wip-g' into wip-ghf
claude Nov 5, 2026
cb8ffc6
Merge branch 'worktree-agent-a913154fab2b1902e' into wip-ghf
claude Nov 5, 2026
89edfeb
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 5, 2026
3d8f149
Merge origin/main (v3.1.5) into the branch
claude Nov 5, 2026
024115d
Merge #411 (square pixels, text wrapping, geometry audit) into wip-ghf
claude Nov 5, 2026
13cfd74
Merge branch 'wip-ghf' into claude/ffmpeg-skill-repo-setup-cn8xm4
claude Nov 5, 2026
9e267bb
freeze, reverse: clone frames before setpts so the hold survives FFmp…
claude Nov 5, 2026
396d83f
Address CodeRabbit's review of #416
claude Nov 5, 2026
80586c3
broll: B's sound lasts the whole cutaway when --from falls between fr…
claude Nov 5, 2026
1ee422c
loop: refuse an output that is its input before the concat list
claude Nov 5, 2026
4c2e0ab
color --lut: keep a BT.601 source's matrix on the ProRes path
claude Nov 5, 2026
f12b1e4
Subtitles, transcription, timeline export, Windows fonts and contract…
claude Nov 5, 2026
000229e
Dry-run honesty, process handling, faststart and batch/render orchest…
claude Nov 5, 2026
0f3dd9a
loop.py --crossfade: seamless repeats, and every loop measures its se…
claude Nov 5, 2026
a2bea69
export.py: DNxHR editing-master presets, and masters read their codec…
claude Nov 5, 2026
1ff22d6
loudness.py --versions: one master, one measured file per loudness ta…
claude Nov 5, 2026
2b405f6
export.py: alpha masters that read their alpha back (#417)
claude Nov 5, 2026
b7b2c58
export.py --preset key-fill: a switcher's fill and key from one alpha…
claude Nov 5, 2026
10532b8
look.py --guides: broadcast safe-area outlines on the preview (#423)
claude Nov 5, 2026
1d84f6b
test: the filler keep range is the rendered one since #416 (#414)
claude Nov 5, 2026
210e129
Merge remote-tracking branch 'origin/main' into wip-i2
claude Nov 5, 2026
95815dc
docs: refresh the design-decisions contents list (#414)
claude Nov 5, 2026
cbe34a9
docs: the #414 decisions ship in 3.1.7
claude Nov 5, 2026
f9463ea
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 5, 2026
e815e00
Merge origin/main (v3.1.6) into the branch; CHANGELOG from main
claude Nov 5, 2026
6061574
Merge branch 'wip-i2' into claude/ffmpeg-skill-repo-setup-cn8xm4
claude Nov 5, 2026
8f6de10
test: the fake whisper-cli fixture runs on Windows too (#414)
claude Nov 5, 2026
770b906
docs: the #415 decisions ship in 3.1.8
claude Nov 5, 2026
2f57e73
Address the #427 review: margin input, filler message, SRT grammar, m…
claude Nov 5, 2026
53e0b97
probe: skip a start timecode whose frames field the picture's rate ca…
claude Nov 5, 2026
1321b0a
probe: validate a timecode's frames field at avg_frame_rate first, as…
claude Nov 5, 2026
7ea082b
Merge remote-tracking branch 'origin/main' into wip-j2
claude Nov 5, 2026
0abe905
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 5, 2026
3ad41b7
CHANGELOG from main (v3.1.7)
claude Nov 5, 2026
cfbc178
Merge branch 'wip-j2' into claude/ffmpeg-skill-repo-setup-cn8xm4
claude Nov 5, 2026
f9cbef2
Address the #428 review: stopped-child exit codes, batch delivery, co…
claude Nov 6, 2026
ab4e2a9
Address the #428 review: dry-run output check, signal exit codes, bat…
claude Nov 6, 2026
47830ac
Refuse a batch output_dir that is the input folder (#428 review)
claude Nov 6, 2026
2dd5feb
Contract: a render-project batch recipe always writes its artifact (#…
claude Nov 6, 2026
9817eff
Keep batch rows' output a string; mark a write-free recipe with writt…
claude Nov 6, 2026
bdcf960
batch: key the cache on the files a step names; fix the QC note (#428…
claude Nov 6, 2026
efa3368
Merge main (v3.1.8) into the working branch
claude Nov 6, 2026
8965367
Take main's CHANGELOG after the v3.1.8 release
claude Nov 6, 2026
96a8bf7
Merge main (v3.1.8) into the feature branch
claude Nov 6, 2026
2fc6682
test: an encoder capability name may contain a hyphen (libvpx-vp9)
claude Nov 6, 2026
fb58d2a
Merge the feature branch (#417 #418 #419 #421 #422 #423)
claude Nov 6, 2026
ea2b50f
loop.py --crossfade: restate the frame rate before xfade (FFmpeg 7.x)
claude Nov 6, 2026
f23b583
Address the #430 review: key-fill outputs, alpha checks, --versions e…
claude Nov 6, 2026
9fbb793
Address the #430 review: loudness, look, demos, loop, key-fill, alpha
claude Nov 6, 2026
aff8e3b
Fix the demo page test off Linux; address the #430 follow-up review
claude Nov 6, 2026
b8204a8
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 6, 2026
da8d29b
Merge main (v3.2.0) into the working branch
claude Nov 6, 2026
4bb594a
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 7, 2026
ad5cca8
Merge origin/main (v3.2.3) into claude/ffmpeg-skill-repo-setup-cn8xm4
claude Nov 7, 2026
f6ffb81
release: find a squash commit's PR by its SHA when the subject does n…
claude Nov 10, 2026
6038492
source_carry: every result says what the output lost from its source …
claude Nov 11, 2026
a6112fe
Fix caption.py --srt --position (#436) and odd-sized JPEGs through th…
claude Nov 11, 2026
61c750f
Merge the OpenSSF Scorecard workflow and MCP registry manifests (#369…
claude Nov 11, 2026
3b33b47
CHANGELOG: the mcp subcommand, the registry manifest and the Scorecar…
claude Nov 11, 2026
0450874
Review of #435: keep an odd frame's own size through the BT.709 conve…
claude Nov 11, 2026
b6dbf2c
Sweep a reported defect across the tools before fixing it: tests/swee…
claude Nov 11, 2026
9f524d9
Fix odd-sized clips in caption.py and stabilize.py (#438)
claude Nov 11, 2026
5c83110
Plugin installs in Codex and Gemini CLI find the skill: skills/ffmpeg…
claude Nov 11, 2026
9962e1a
Review of #435: bound each sweep child, refuse a failed probe, write …
claude Nov 11, 2026
06f6983
CONTRIBUTING: Smithery's publish commands no longer exist (its API an…
claude Nov 11, 2026
46dd9f0
Publish a plugin branch with only the runtime set on each release
claude Nov 11, 2026
bc430e3
sweep.py: --list needs no input; pin its probe and timeout handling; …
claude Nov 11, 2026
78971a4
Record the questions an outside reviewer kept asking, as a pre-push list
claude Nov 11, 2026
753b800
Plugin branch step: do not hold the npm publish back; tell a missing …
claude Nov 11, 2026
7f1e9e2
reviewer-findings: path guards hold in dry runs; side-file order depe…
claude Nov 11, 2026
7c0ba79
Merge main (v3.3.0) into the working branch
claude Nov 11, 2026
c955ecf
check.py --compare --expect-changed: fail on a picture change outside…
claude Nov 11, 2026
1ebf86c
change_scope: grade overlapping declarations independently; refuse a …
claude Nov 11, 2026
e955d68
Refuse nan and inf for every float flag of every tool, and for every …
claude Nov 11, 2026
c8be17d
test: --change-floor 254 is asserted by what the document reports, no…
claude Nov 11, 2026
e044d2f
Non-finite flag values: a failure document with --json, as the flags …
claude Nov 11, 2026
9f5d47f
change_scope: rectangles are in the displayed picture (non-square pix…
claude Nov 11, 2026
02f6490
change_scope: equal displayed sizes are compared whatever the stored …
claude Nov 11, 2026
8970861
change_scope: the stored-size note reads the file's own width and hei…
claude Nov 11, 2026
1ca5780
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
896d9ad
Sync CHANGELOG with main after the 3.4.0 release
claude Nov 11, 2026
f2081fe
Marketplace entry names the plugin branch's ffmpeg-skill/ directory
claude Nov 11, 2026
8913c87
README: the plugin route's minimum Claude Code version for a git-subd…
claude Nov 11, 2026
987d94b
README: a table of the install routes that work today, and the direct…
claude Nov 11, 2026
d6d9750
Plugin tree: ship the listing icon, leave the npx installer out
claude Nov 11, 2026
434092f
README: the one-line plugin install needs 2.1.275; older versions use…
claude Nov 11, 2026
1671406
Plugin tree: a short README of its own; workflow.md says add --overwrite
claude Nov 11, 2026
d7b325d
Plugin README: transcription engines may download a model and use a l…
claude Nov 11, 2026
6d11c62
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
8781cb5
Sync with main after the 3.4.1 release
claude Nov 11, 2026
9b6ac28
Plugin icon: picture chunks only, pixels recompressed; drop mcpName f…
claude Nov 11, 2026
0ef2378
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
d1617dd
Sync with main after the 3.4.2 release
claude Nov 11, 2026
62bdf66
Plugin listing: displayName, documentation, support and privacy links…
claude Nov 11, 2026
f5d9299
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
521fd46
Sync with main after the 3.4.3 release
claude Nov 11, 2026
6e2da95
release.yml: publish the MCP Registry entry after npm, by OIDC
claude Nov 11, 2026
ed5f9ea
release.yml: an npm that cannot be asked is not 'not on npm'
claude Nov 11, 2026
a3de19c
release.yml: a tagged version npm does not have fails the registry step
claude Nov 11, 2026
4e487b9
release.yml: report both publish failures; recovery text without a fl…
claude Nov 11, 2026
8caaf3f
release.yml: ask the MCP Registry for the exact name and version
claude Nov 11, 2026
649fe2e
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
1f49479
README: the MCP Registry entry is live
claude Nov 11, 2026
3ee1125
README: the registry row says what the release workflow does when its…
claude Nov 11, 2026
381bbac
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
ef44c0a
test: read the icon word list from the publish script and bound the P…
claude Nov 11, 2026
34824d0
test: keep the seven known icon words as a fixed minimum
claude Nov 11, 2026
f1e5f0e
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 11, 2026
1ba8b6f
feat(silence): --margin-lead and --margin-tail keep different amounts…
claude Nov 12, 2026
2b65364
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 12, 2026
c695bc3
feat(silence): --sentence-min-silence and --sentence-margin-tail for …
claude Nov 12, 2026
3e637ac
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 12, 2026
8fe593a
docs(changelog): keep the sentence-flags entry under Unreleased after…
claude Nov 12, 2026
ea923be
fix(silence): tell the word before a pause by word midpoints, and tes…
claude Nov 12, 2026
3b83636
fix(silence): measure an open-ended silence, refuse unusable word tim…
claude Nov 12, 2026
718506b
fix(silence): a silence that runs to the end of the file ends at the …
claude Nov 12, 2026
79f4907
test(silence): the sentence test makes its own clip
claude Nov 12, 2026
47a962a
docs(silence): margins is present under --filler when a sentence flag…
claude Nov 12, 2026
31cce57
Merge remote-tracking branch 'origin/main' into claude/ffmpeg-skill-r…
claude Nov 12, 2026
200dc16
chore: sync the branch with main after #452 and the 3.6.0 release
claude Nov 12, 2026
31dfd4d
fix(security): refuse DASH/IMF manifests by content, subfile URLs and…
claude Nov 12, 2026
8998cc0
fix(security): export.py's fill temp and batch.py's delivery temp are…
claude Nov 12, 2026
8011011
fix(security): copy through the reserved descriptor, refuse the 3GPP …
claude Nov 12, 2026
a2cadfa
fix(security): guard the project file, resolve overlay fonts, fail cl…
claude Nov 12, 2026
a93e1ef
fix(security): verify the default audit log after opening it where O_…
claude Nov 12, 2026
e7d6cbe
fix(security): guard the brand's own logo and font, skip the output g…
claude Nov 12, 2026
e337c57
fix(security): refuse a hard-linked default audit log where O_NOFOLLO…
claude Nov 12, 2026
e13aa91
test: require the --stop-after preview to succeed, not only to avoid …
claude Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,44 @@

## Unreleased

(nothing yet)
### Security

- **A DASH manifest or an IMF composition playlist given as media is refused, whatever it is
called.** The playlist guard knew HLS and ffconcat by name and first bytes only, so a DASH
manifest saved as `clip.mp4` (with a `dash:profile` or the 3GPP `PSS:profile:DASH1`) made every
tool follow its `BaseURL`: to another local file, which
the output then contained (reported completed and verified), or over HTTP, which FFmpeg 6.1's
`-protocol_whitelist file` did not stop. Both are now told the way ffmpeg's own probes tell
them, by content, and refused as `media_path_is_playlist`.
- **`subfile,<options>:<url>` is refused as a URL.** ffmpeg opens it through the subfile protocol;
the URL guard stopped reading the scheme at the comma and let it through, so the guards
described the named file while ffmpeg read another.
- **A path with a line break is refused where it would go into a concat list** (`loop.py`,
`join.py --copy`, `sequence.py`, multi-part cuts): `media_path_has_line_break`. Each line of a
concat list is a directive, so such a name added entries and pulled a file named nowhere on the
command line into the output.
- **The hidden temp files in the output directory are created exclusively.** The staging temp of
an `--overwrite` run, `place_output()`'s temp, `export.py`'s key/fill temp and `batch.py`'s
cross-filesystem delivery temp had fixed names and were opened through
whatever was already there, a planted symlink included: the encode was written into the link's
target and the output left as a symlink to it. A copy the tool makes itself is written
through the descriptor that created the temp, never by reopening its name, and a directory that
refuses the exclusive create fails the run (`output_dir_not_writable`) instead of handing ffmpeg
a name nobody reserved. A lock's default audit log is opened without following a symlink.
- **`render.py` refuses an output that is any file the project names.** The guard compared the
output with the clips and overlay videos only, so with `--overwrite` a project whose output was
its own music bed, brand file (or the brand's logo or font), overlay image or font, caption
file, beat source, or the project file itself replaced it. A `--stop-after` preview, which
never writes the output, is not refused. `audiogram.image` and an overlay's `font_file` are now passed
to their stages resolved against the project file, as every other project path is, instead of
against the caller's working directory.

### Fixed

- **A colour given to `caption.py`, `graphics.py` or a brand must be six hex digits.** The check
was on the length only: a six-letter name such as `yellow` or `orange` drew black text and was
reported verified, and a quote or comma reached the filter graph and failed as `kind: ffmpeg`.
It is now a `kind: input` refusal.

## 3.6.0

Expand Down
2 changes: 1 addition & 1 deletion docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ here claims otherwise until real exit-code/stderr sniffing exists to back that u
A refusal also carries `error.reason`, a stable snake_case code for *why*
(`contract --json` → `json_output.error_reasons` lists them: `output_exists`, `output_is_input`,
`output_is_directory`, `output_dir_missing`, `output_dir_not_writable`, `output_locked`,
`media_path_is_url`, `media_path_is_playlist` (an HLS or ffconcat playlist given as media),
`media_path_is_url` (`subfile,...:` included), `media_path_is_playlist` (an HLS, DASH, IMF or ffconcat playlist given as media, DASH and IMF told by content whatever the file is called), `media_path_has_line_break` (a path with a line break, which a concat list cannot hold),
`output_looks_like_option` (an output starting with `-`, other than `-` for stdout), `output_path_taken` (two files one run writes share a path), `media_path_not_a_file` (a FIFO or device as input), `silence.py --timemap`'s `timemap_path_taken`, `join.py --copy`'s `join_copy_mismatch`,
`render.py`'s `plan_*` reasons, `--codec av1`/`prores`'s `encoder_missing` and
`encoder_list_unreadable`, and `--transcribe`'s `asr_engine_missing`, `no_speech`, `engine_failed`
Expand Down
24 changes: 23 additions & 1 deletion docs/design-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,29 @@ exists. When a decision changes, edit the entry in the same PR.
- **An existing output is written through a hidden sibling temp file and replaced only on
success**, so a failed run never costs the caller the file that was there (FFmpeg 5.x truncates
the output before a filter error). The temp name `.<stem>.ffskill-<pid><ext>` is expected in the
output directory during a run. Code: `_common._stage_existing_output()`.
output directory during a run; it is created exclusively (`O_EXCL`), so an entry already at that
name, a planted symlink included, is never written through, and the next free
`.<stem>.ffskill-<pid>-<n><ext>` is used instead. A copy the tool makes itself (`place_output`,
`batch.py`'s cross-filesystem delivery) writes the descriptor that created the temp
(`copy_to_sibling()`); ffmpeg opens its temp by name, so a process that may delete entries in
the output directory could still swap the file between the create and that open, which a
sticky shared directory (`/tmp`) and a directory only the caller can write both rule out. A lock's default audit log beside the output
is opened without following a symlink; a log named with `--audit-log` is opened as named. Code:
`_common._stage_existing_output()`, `runner.reserve_sibling()`, `emit.audit()`. Test:
`test_hidden_temp_names_are_never_written_through_a_planted_link`.
- **A file ffmpeg would read as a playlist is refused whatever it is called.** HLS and ffconcat
are told by name or their first bytes; a DASH manifest and an IMF composition playlist by name
or by the strings ffmpeg's own probes look for (`<MPD` with `dash:profile`, `<CompositionPlaylist`
with `ContentTitle>`), anywhere before the first NUL byte, because ffmpeg probes the whole probe
buffer: a manifest saved as `clip.mp4` made every tool follow its BaseURL to another local file
or over HTTP, and FFmpeg 6.1's `-protocol_whitelist file` did not stop the HTTP request. A media
file has a NUL byte within its first bytes, so the scan stops there. `subfile,<options>:<url>`
is refused as a URL like any other protocol prefix. A path with a line break is refused where it
would be written into a concat list (`media_path_has_line_break`): the format has no escape for
one. Code: `runner._refuse_playlist()`, `runner.looks_like_url()`, `decision.concat_list_line()`.
Tests: `test_a_playlist_input_and_an_option_shaped_output_are_refused`,
`test_a_url_never_reaches_ffmpeg_as_an_input_or_an_output`,
`test_a_path_with_a_line_break_never_reaches_a_concat_list`.
- **Subtitle/data tracks are kept where the picture's timeline is untouched and dropped where
it is retimed; both cases report `dropped_non_av_streams`.** Tools that re-encode the picture
or audio in place (`fit`, `color`, `graphics`, `overlay`, `audio`, `loudness`, `proxy`,
Expand Down
2 changes: 1 addition & 1 deletion scripts/_ass_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def ass_time(sec: float) -> str:

def ass_color(hex_rgb: str, alpha: int = 0) -> str:
h = str(hex_rgb).lstrip("#")
if len(h) != 6:
if len(h) != 6 or any(c not in "0123456789abcdefABCDEF" for c in h):
raise ValueError(f"colour must be RRGGBB hex, got '{hex_rgb}'")
return f"&H{alpha:02X}{h[4:6]}{h[2:4]}{h[0:2]}".upper()

Expand Down
13 changes: 10 additions & 3 deletions scripts/_common/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,20 @@ def through(m: "re.Match[str]") -> str:
return out[:at] + ["-vf", conv] + out[at:]


_HEX6 = re.compile(r"^[0-9A-Fa-f]{6}$")


def color_hex(value: str) -> str:
"""Normalise '#ffd200' / 'ffd200' / '0xFFD200' to 'FFD200'."""
"""Normalise '#ffd200' / 'ffd200' / '0xFFD200' to 'FFD200'. Six hex digits only: the result is
written into libass styles and drawtext/color options, so a six-letter colour name ("yellow")
used to pass the length check and draw black, reported verified, and a quote or comma reached
the filter graph."""
v = str(value).strip().lstrip("#")
if v.lower().startswith("0x"):
v = v[2:]
if len(v) != 6:
die(f"colour must be RRGGBB, got '{value}'")
if not _HEX6.match(v):
die(f"colour must be six hex digits RRGGBB (e.g. FFD200 or #ffd200), got '{value}'; colour names are not "
"accepted here")
return v.upper()


Expand Down
10 changes: 9 additions & 1 deletion scripts/_common/decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,15 @@ def concat_list_line(path: str) -> str:
"""One `file '...'` line for the concat demuxer. The demuxer reads backslash as an escape
inside the quoted form, so a Windows path (C:\\Users\\...\\part000.mp4) must be written
with forward slashes -- ffmpeg opens either spelling on Windows -- and a single quote in the
name is closed, escaped and reopened. Shared by cut.py (multi-segment) and sequence.py."""
name is closed, escaped and reopened. Shared by cut.py (multi-segment) and sequence.py.

A line break has no escape in this format: the demuxer reads each line as its own directive,
so a name containing one would add `file` entries to the list and pull in files the caller
never named. Such a path is refused."""
if "\n" in str(path) or "\r" in str(path):
refuse("input", "media_path_has_line_break",
f"{str(path)!r} contains a line break, which ffmpeg's concat list cannot hold (each line is a "
"directive of its own); rename the file", path=str(path))
# only on Windows: on POSIX the quoted form takes a backslash literally, and it is part of the name (#408)
escaped = (str(path).replace("\\", "/") if _on_windows() else str(path)).replace("'", "'\\''")
return f"file '{escaped}'"
Expand Down
24 changes: 23 additions & 1 deletion scripts/_common/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import json
import math
import os
import stat
import sys
from typing import Any, Dict, List, Optional, Sequence, Tuple
from _common.runner import Context, ERROR_CODE, ERROR_RETRYABLE, STATE
Expand Down Expand Up @@ -319,7 +320,28 @@ def audit(event: str, details: Dict[str, Any], *, default_path: Optional[str] =
rec["plan_sha256"] = plan_sha256
rec["details"] = details
try:
with open(path, "a", encoding="utf-8") as f:
# a default path is a hidden name in someone's output directory: never appended to through a
# symlink planted there. A log the caller named (--audit-log) is opened as named.
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT | (0 if ctx.audit_log else getattr(os, "O_NOFOLLOW", 0))
Comment thread
kajisho5 marked this conversation as resolved.
Resolved
checked = not ctx.audit_log and not hasattr(os, "O_NOFOLLOW")
if checked and os.path.islink(path):
raise OSError(f"{path} is a symbolic link")
fd = os.open(path, flags, 0o666)
if checked:
# no O_NOFOLLOW (Windows): a link that appeared between the look above and the open is
# caught after it -- the descriptor must be the file now at that name, not a link's
# target -- and nothing is written through it
try:
st, at = os.fstat(fd), os.lstat(path)
# st_nlink: a link swapped for a hard link to its target before the lstat would
# otherwise pass the identity check; a log this tool created has one name
if (stat.S_ISLNK(at.st_mode) or (st.st_dev, st.st_ino) != (at.st_dev, at.st_ino)
or not stat.S_ISREG(st.st_mode) or st.st_nlink != 1):
raise OSError(f"{path} was replaced by a link while it was opened")
except BaseException:
os.close(fd)
raise
with os.fdopen(fd, "a", encoding="utf-8") as f:
f.write(json.dumps(json_safe(rec), ensure_ascii=False, sort_keys=True) + "\n")
except OSError as exc:
warn(f"could not append the {event} record to the audit log {path} ({exc}); the run is unaffected",
Expand Down
Loading
Loading