Repository navigation
Release v0.33.0: caption coverage and cache, delivery gates, draft-change diff, shareable fixtures - #172
Merged
Conversation
- The CapCut cloud clients share one request routine with retries. - CapCut's text colour and fill format is written by one set of helpers. - Language-code normalization drops a fallback loop that could never return. - caption resolves, extracts and cleans up its audio in one helper.
loudness always answered ok:true and exited 0, so a script could not stop an export whose mix misses its delivery target. --tolerance <LU> (needs --target) fails the run when the estimated loudness sits further than that from the target; --max-true-peak <dBTP> fails it when the loudest clip's true peak, at its volume, is above the ceiling. With --apply the mix after this run's gain is judged (the --max-gain-db cap included), without it the mix as it stands. A miss exits 1 with ok:false and one reason line on stderr, and the result carries a gate object with the target, tolerance, estimate and each verdict. With --apply the gain is still written, as lint --fix writes its repairs before reporting what remains. Without either flag the result and exit code are unchanged. The verdict is computed by judgeLoudness beside measureLoudness, which checks the new options before any clip is measured.
lint --max-still-secs <n> reports still-too-long (warning) for each segment whose material is a still image (a photo) and plays longer than n seconds on the timeline; --max-shot-secs <n> reports shot-too-long (warning) for each video clip longer than n seconds, overlay tracks included. Messages name the segment, its track, where it starts and how long it plays. Neither is fixable, and neither runs unless its flag is given, so the default lint result is unchanged. A threshold that is not a positive number of seconds is refused instead of switching the check off silently.
lint --min-silent-secs <n> reports silent-stretch (warning) for every range of n seconds or longer, up to the end of the timeline, where nothing audible plays: no audio-track clip above volume 0 and no video clip that carries its own sound. The check is conservative so it never reports silence that might not be there. A clip counts as silent only when the draft says so: volume 0 without volume keyframes, a muted track (attribute bit 1), a photo, or a video whose sound was separated to an audio track. A video clip whose file may lack an audio stream, or whose material is missing, counts as sound. The end of the range is the draft duration, clamped to the last segment on any track. Not fixable, and off unless the flag is given.
After caption transcribes (local Whisper or --cloud), measure the
transcribed audio with ffmpeg silencedetect, subtract the cue spans in
the audio's own time and report coverage {speech_s, captioned_s, ratio,
uncaptioned, tail_uncaptioned_s} in the result. Uncovered speech of at
least --min-uncaptioned seconds (default 1.5) is listed and printed as
one [uncaptioned-speech] warning on stderr, so a transcription that
stops early or drops quiet speech becomes visible. The exit code is
unchanged. Without ffmpeg the block is {skipped: "ffmpeg not found"};
--no-coverage skips the check and --words runs have none.
…again caption stores the raw transcription (cues, words, engine; before --script, grouping, styling and --translate) keyed by the sha256 of the exact audio bytes transcribed plus engine, whisper command, model, language, word-timing mode, --cloud and a cache format version. A run that only changes the later steps reads it back: no engine run, and with --cloud no upload. The result reports transcript_cache hit, miss or off. Entries are one JSON file per key in CAPCUT_CLI_CACHE_DIR, else $XDG_CACHE_HOME/capcut-cli/transcripts, else ~/.cache/capcut-cli/transcripts (%LOCALAPPDATA% on Windows), written atomically; an unreadable or mismatched entry is ignored and rewritten. --no-cache bypasses it and --dry-run never writes it. The test helper isolates the cache dir per spawn.
render --burn-captions passed drawtext no font file, so every burned
caption used ffmpeg's default face. Each caption now uses its text
material's font: the first style block's font.path in the content,
else the material's font_path, passed as drawtext fontfile= when the
file exists and is a .ttf, .otf or .ttc. The path is escaped for both
filtergraph parsing levels and quoted, so commas, brackets, colons and
backslashes stay inside the option; a path with a quote or a control
character is not used. Anything else keeps the default face. The
render result and --dry-run plans report caption_fonts {from_draft,
default}.
fixture --out now refuses an occupied destination, an output symlink and any path inside the source project before writing anything. The bundle is built and redaction-checked in a temporary sibling directory and moved into place only when complete, so a failed run leaves a new destination absent and an empty one empty.
…s about fixture --anonymize gives media files neutral names (media-001.<ext>, the same file the same name in every mirror), replaces folder names on media and project paths with folder-001 (standard folders are kept), sets the project name to "project" and replaces every text character by character with placeholders of the same UTF-16 length and script, so style ranges and line wrapping still behave as in the original. Only the replaced strings change; every other byte of each file is kept. The report records anonymized: true and counts what was replaced, and the redaction check runs on the anonymized bundle.
…st write diff <project> --since-write compares the draft on disk with what the CLI itself last wrote to it, so changes the app made when it opened or saved a tool-written draft show up in the usual diff result (a = as written, b = now) plus a since_write block with the write time, both sha256 digests, whether the file changed, the top-level fields that differ and whether the tracks were reordered. A draft with no recorded write exits 1. Every write now keeps a copy of its result as <file>.last-write in the draft's history directory, one per draft file, replaced atomically on each write and on restore. It is not a snapshot: restore, restore --list and the history cap ignore it. diff <a> <b> is unchanged.
…ange diff, shareable fixtures
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nine changes for v0.33.0 on top of a behavior-preserving tidy-up, plus the version bump. The first commit is the tidy-up on its own, so it can be reviewed separately from the features.
Captions
captionchecks that its cues cover the speech it transcribed. ffmpeg's silencedetect measures the non-silent spans of the transcribed audio, the cue spans are subtracted, and the result carriescoveragewith every uncaptioned stretch of at least--min-uncaptionedseconds (default 1.5). A[uncaptioned-speech]warning names them, so a transcription that stops before the end of the audio, or drops quiet speech, no longer passes unnoticed. Exit code unchanged; skipped with a note without ffmpeg;--no-coverageturns it off.captioncaches what it transcribed, keyed on the audio's content and the engine settings, so a run that only changes styling, grouping,--scriptor--translatedoes not transcribe again (and--clouddoes not upload again).transcript_cache: hit | miss | offin the result;--no-cachebypasses it;CAPCUT_CLI_CACHE_DIRmoves it.render --burn-captionsdraws each caption in its text material's own font file instead of ffmpeg's default face, falling back as before when the file is missing.Delivery checks
loudness --tolerance <LU>(with--target) and--max-true-peak <dBTP>gate a delivery: a miss exits 1 withok: false, andgatecarries the estimate and each verdict. Without the flags the result and exit code are unchanged.lint --max-still-secs/--max-shot-secsreportstill-too-longandshot-too-long;lint --min-silent-secsreportssilent-stretchwhere nothing audible plays. All opt-in, so lint's default result is unchanged.Compatibility evidence
diff <project> --since-writeshows what changed in a draft since capcut-cli last wrote it, such as what CapCut rewrote on open or save. The CLI keeps one record of its last write per draft file in its history directory; snapshots,restoreandrestore --listbehave as before.fixture --outrequires a new or empty directory outside the source project, builds the bundle in a temporary sibling and publishes it only when complete.fixture --anonymizebuilds a bundle that can be shared without showing what the draft is about: neutral media, folder and project names, consistent across every mirror, and text replaced character by character so lengths, scripts and style ranges behave the same.Fixed
Tidy-up (first commit): the CapCut cloud clients share one request routine, CapCut's text colour format is written in one place, a language-lookup fallback that could never return is removed, and
captionresolves, extracts and cleans up its audio in one helper. CLI help,describe, the library exports, shipped type files and the packed file list are identical before and after it.Evidence
npm run build,npm test,npm run lint(biome, as CI runs it),python -m unittest discover -s python/tests, the packed-tarball install smoke andnpm run wasm:verifypass on the final tree with Node 22 on macOS: 1,477 tests, 1,474 pass, 0 fail, 3 skipped (the new real-render font test skips where ffmpeg has no drawtext).--no-cache, corrupt entry, cloud hit without upload), caption fonts (resolution, escaping, fallbacks), loudness gate (tones at known levels,--apply, peak ceiling), lint pacing and silent stretches, fixture output isolation, fixture anonymization (all mirrors, CJK/Latin/kana/Hangul text, style ranges), anddiff --since-write(track reorder, value change, added material, unchanged, no record, restore unaffected).0f58b834a1188bd810cbf5325ac8cb6480134123(master). Touched: caption, loudness, lint, render, fixture, diff/history write path, docs, CHANGELOG, README version note. Untouched: draft write format, store selection, editor guard, Python client.package.jsonandpackage-lock.jsondev-dependency lines; this branch changes only their version field.Door
Two-way. Every change is additive or opt-in except two defaults:
captionnow adds acoveragefield and a stderr warning, and it reuses a cached transcription for identical audio and settings (--no-cacherestores the old behaviour). Reverting the release commit range restores 0.32 behaviour; the only stored artifacts are the transcript cache in the user cache directory and one last-write record per draft file in.capcut-cli-history/, both safe to delete.Blast radius
caption(all engines),loudness,lint(only with the new flags),render --burn-captions,fixture,diff --since-write, and every CLI write, which now also refreshes its last-write record next to the existing history snapshots. Users who parsecaptionoutput strictly see two new fields (coverage,transcript_cache).