1. Positioning
This skill handles final assembly:
- Place each narration audio segment onto the video timeline.
- Within narration windows, use a fixed envelope to duck the original sound (one volume level when covering original dialogue and another when falling on quiet passages), and restore the original sound in the gaps.
- Generate based on narration positions
subtitles.srt; by default also generate and burn insubtitles.ass,--no-burn-subtitlescan be disabled. When not burning in (disabled or degraded),subtitles.srtcopy next to the cut, namedrecap_<stem>.srt; when burning in, delete the old external subtitle with the same name. - Optionally normalize the final loudness to the target LUFS: two-pass loudnorm, using only a constant gain, with no dynamic compression; when the mix's true peak cannot accommodate such a large gain, first pass through a 4x oversampled true-peak limiter (cutting at most
LOUDNESS_LIMITER_MAX_DB, default 6 dB), and only lower the target loudness for the excess. The true-peak target is for the delivered AAC file: AAC encoding can make the true peak 0.3–1.4 dB higher than the PCM mix, so the first render aims for 0.5 dB below TP, and after rendering it is decoded and measured; if it still exceeds -1 dBTP, lower by another 0.1 dB based on the excess and re-encode only the audio (video stream copied), at most twice; if it still exceeds, block withdelivered_true_peak_over_target. The mode actually used by ffmpeg, the limiter amount, and the measured values of the final cut are recorded inassembly_qc.jsoninloudness_modeandloudnorm_final_pass(delivered)。 - The final cut does not carry the source's container metadata (
title、commentand other tags and chapters).
2. Sound finishing contract
The assembly stage only implements creative decisions, it does not create decisions out of thin air. Before writing narration positions, the Agent has already, in visual_audio_board.json for each beat, specified audio_owner:
original_dialogueaction_soundambience/musicsilencenarration
Therefore, narration gaps are active choices, not blanks that must be filled. Do not add generic BGM to make it "fuller," suppress lines that must be heard, or eliminate meaningful silence.
The current renderer does not parse visual_audio_board.json; the Agent implements these decisions through narration timing,overlaps_speech, original-sound pauses, and existing mixing parameters.
3. Input contract
<video>: source video; in cut mode it isedited_source.mp4。work_dir/tts_meta.json: defaultnarrationrequired for mode; written by the dubbing stage{segments: [...]}. Each segment containsaudio_path, time,pause_after_ms、overlaps_speechand positions used for mixing/subtitles. Explicitsource-mix/adopted-packet-copymode does not read it.- Adopted dubbing uses explicit
--tts-metaand--narration-adoption: the latter is independently confirmed by the caller regarding text, requested engine/voice, and speed policy, and cannot be automatically "approved" from metadata to be consumed. For the complete format and record boundaries, seereferences/narration-adoption.md。 - The adopted complete sound bed and per-segment dubbing can additionally pass
--audio-mix-adoption; for the strict format, 48 kHz channel matrix, and dual binding transaction, seereferences/explicit-audio-mix.md。
Below, the scripts/... are all relative to this skill directory. If the executor is started from the repository root, prepend the skill's absolute directory to the script path.
4. Run command
python3 scripts/assemble.py <video> --work-dir <work_dir> \
[--audio-mode narration|source-mix|adopted-packet-copy] [--audio-stream-index <N>] \
[--tts-meta <tts_meta.json> --narration-adoption <narration_adoption.json>] \
[--audio-mix-adoption <audio_mix_adoption.json>] \
[--recap-stem <name>] [--output-dir <dir>] [--no-burn-subtitles] \
[--subtitle-y-top <inclusive-y> --subtitle-y-bot <exclusive-y>] \
[--source-video <orig.mp4>] [--export-jianying [--jianying-out <dir>]]
5. Output contract
recap_<stem>.mp4: stable final output alias; overwritten and updated on each run.work_dir/output.mp4: the cut within the working directory.subtitles.srt: narration subtitles; when burning in, there is alsosubtitles.ass。timeline.json: Backend-agnostic multi-track model, including video, original sound, narration, BGM, subtitles, and ducking automation._placed_*.wav: The complete segment-by-segment narration PCM actually written to the main mix; the timeline and CapCut only reference these files.narration_input_binding.json: Consumption record of narration input, conversion, actual placement, narration master track, and final audio track (paths, PCM parameters, packet counts). Distinguishes the two states of not adopted and bound-adopted decisions; it is not equal to voice identification or listening review.audio_mix_binding.json: Record of the picture clock, base track, 48 kHz dubbing placement, premaster, fixed master gain, final PCM/AAC facts, and narration binding paths explicitly consuming the complete audio branch.assembly_manifest.json: Input source, cut source identifiers (path, size, mtime), render settings, and final output path.assembly_qc.json: Release gates for narration integrity, original-sound sentence-end handoff, timeline asset duration, and delivery quality.- CapCut draft directory: generated only when
--export-jianying, includingdraft_content.json、draft_info.jsonanddraft_meta_info.json。
6. Composition rules
- For the processing and freezing semantics of audio modes, see
references/audio-modes.md. The default remainsnarration; the other two modes must be explicitly selected. --audio-mix-adoptionis used only together with explicit--tts-meta、--narration-adoption; it retains thenarrationmode name, but skips the old speed/fitting, original-sound handoff, ambient BGM, duck, loudnorm, and limiter.- Audio is mixed by track: original sound, optional BGM, and narration are each independent.
- For a segment's first block (with more than 1.6 seconds of author-intended blank space from the previous block), place from the written
start, but no earlier than the previous block's actual end plus itspause_after_ms; if the previous block runs overtime, this pushes it later; subsequent blocks within the segment follow immediately after the previous block's actual end (0.35 seconds apart), at most 1.2 seconds earlier than the writtenstart. This advancement happens after narration validation, so the advanced segment cannot enter the original-sound dialogue: when there is dialogue, it can be advanced at most to the end of the last dialogue segment. Blocks that were actually advanced recordsource_entry_statusinparagraph_tightened, and record the writtenwritten_startinstart。 - Narration does not undergo any tolerance-based tail trimming; if it still does not fit after gentle speed-up, then
no_safe_fit. Each segment's_placed_*.wavmust be equal in length to or shorter than the serialized timeline interval, otherwisetimeline_audio_mismatchblocks it. - The v1 contract for adopted dubbing supports only original speed and forbids in-segment speed fitting; the environment default 1.15x speed or old cache must not override it. If it does not fit, revise the arrangement; do not trim the tail. Strict runs use a new working directory and new output path; when inputs/actual mix sources change or QC fails, do not release a candidate final cut. Old entry points without adopted files are still compatibility mode and do not automatically gain equivalent evidence.
- The original sound is ducked for at most another 3 seconds after the narration ends, waiting for the next sentence-end anchor within this period: duck to its
pause_start, ramp up only within the measured pause, and return to full atsource_restore_at(when the anchor isunverified, the state is recorded assentence_boundary_unverified). When there is no anchor within 3 seconds, if less than 3 seconds remain to the timeline end, duck to the end (held_to_timeline_end), otherwise return to full directly at the narration end (bounded_release), and do not hold down the original-sound dialogue for a long time for a distant anchor.assembly_qc.json'ssummary.max_source_duck_hold_secondsrecords the longest ducking continuation. --export-jianying/EXPORT_JIANYING=1can exporttimeline.jsonas an editable draft. Cut mode should pass--source-video <orig>, so the draft references the real original-film intervals.- CapCut export by default copies video, audio, and images to
Resources/local/{video,audio,image}, keeping the draft relocatable;--jianying-no-bundle-mediaonly suitable when the original path is always accessible. - Overlapping overlays are split into numbered tracks; a non-empty target directory is not overwritten, but instead a numbered sibling directory is created.
- Export only accepts
schema_version: 2oftimeline.json, mapping only video, audio, subtitles and image overlays (scale/position); hand-written extension fields such as speed changes, transitions, masks, rich text and effect tracks are explicitly rejected. - The Jianying draft references the unburned source video, so hard subtitles in the original footage remain; mask them separately in Jianying if necessary.
- Subtitle appearance can be controlled with
SUBTITLE_FONT_SIZE、SUBTITLE_MARGIN_V、SUBTITLE_MAX_CHARSetc. SUBTITLE_Y_TOP/BOTplaces the ASS baseline into the measured subtitle area of the original footage, with coordinates as a half-open[top, bot)on the display canvas, accepting only square or near-square pixels (SAR within 2% of 1:1; unlabeled0:1is treated as square); under the explicit mask policy the defaultSUBTITLE_MASK_OPACITY=0.6,SOURCE_SUBTITLE_MASK_TIMING=narration。- The original audio returns to
IDLE_ORIG_VOLUMEin narration gaps, and narration is ducked toSPEECH_DUCKING_VOLUME;DUCK_FADE_SECONDScontrols the transition.DUCK_BRIDGE_SECONDS、ZONE_DUCKING_VOLUME、FINAL_LOUDNORM、TARGET_LUFSandLOUDNESS_LIMITER_MAX_DB。 - BGM can be specified via
BGM_PATH; it loops to the finished length and is mixed according toBGM_VOLUME/BGM_DUCKING_VOLUME. Do not set generic BGM without a creative basis. - Burning subtitles requires ffmpeg with
subtitles/ libass, prechecked before rendering in the compositing stage. When burning is explicitly required (--burn-subtitlesor environment variableBURN_SUBTITLES), missing libass fails directly; when it is merely enabled by default, it degrades: no burn-in, delivering external.srt(the「」original dialogue in the blank space is still written in as usual only when there isuser_subtitles.*; dialogue originally triggered by the mask is turned off together with the mask, and the original hard subtitles are visible),visual_qc.jsonofwarningsandassembly_manifest.jsonofwarningsrecords asubtitle_burn_degraded,subtitles.burn_degraded_reasonwriting the reason. After degradation the mask is still turned off (mask.triggerisburn_subtitles_degraded)。 visual_overlays.jsontext overlays use ffmpegdrawtext(libfreetype). When drawtext is missing, compositing fails before rendering; overlays are content explicitly added when writing the script and will not be silently dropped.- The finished picture is always H.264 8-bit 4:2:0 with
+faststart: stream copy and deliver as-is only when no filters are needed and the source picture is already H.264 8-bit 4:2:0 (yuv420p, or full-rangeyuvj420p) with even width and height; otherwise re-encode toyuv420p. Color tagging: if the source is untagged or already BT.709, tag as BT.709 (-colorspace/-color_primaries/-color_trc bt709); other declared color spaces are preserved as-is; full-range (pc) sources keeppc, the rest are taggedtv. YUV sources only have their tags changed, without pixel conversion; RGB sources (reported by ffprobe asgbr, such as PNG/QuickTime RLE wrapped MOV) have no YUV matrix to preserve and are converted as BT.709 limited. The result is recorded inassembly_qc.jsonofdelivery_qc.color_tags(RGB sources have one extrafrom_rgb: true)。 - The dialogue subtitles in the original-audio blank space preferentially read the Agent-proofread
original_subtitles.json; otherwise the ASR is mapped conservatively. Original dialogue is burned only when the mask covers the blank space or the user subtitles explicitly require replacement, and is distinguished from narration with「」.
Prepare the sound according to the original intervals, rather than lowering the old finished video as a whole
When there are already multiple original-audio choices and independent BGM decisions, first use references/source-score.md of the independent
source_score.py Reconstruct the original sound track, music track, and the sum of both from the original audio stream by precise frame intervals; it outputs only the
sound base track and source receipt. To synthesize with the already-adopted dubbing segment by segment, the caller then provides references/explicit-audio-mix.md
strict adoption; do not stuff the base track into the old entry for automatic ducking, and do not take the entire sound from a finished video containing old narration and pass it off as clean original audio.
7. Subtitles and optional packaging
Lock the picture, cut points, narration, and mix first, then invest in subtitle animation or frame packaging; subtitle styling must not mask problems in narrative, cut points, or sound.
For ordinary delivery, preferentially use the existing ASS path; for frame wrapping, title bars, and corner marks that stay fixed across the whole video, use packaging_layers.json static overlay;
only when the user needs per-cue layout, animation, or transparent layers, use a project-level code renderer, and have that project itself composite the transparent layer onto the locked master.
For the packaging order and sample-frame spot-check list, see references/packaging.md。
8. Capability boundaries
- Does not generate narration text, does not synthesize TTS, does not re-transcribe the video.
- Subtitle burn-in is enabled by default; when disabled, the subtitle drawing area will not be re-encoded.
For the independent contract of the explicit output-axis subtitle track, the full replacement semantics, and the current boundaries, see references/subtitle-track.md。