1. Positioning
This skill handles final synthesis:
- Place each narration audio segment onto the video timeline.
- Within narration windows, use a fixed envelope to duck the original audio (one volume level each for when it covers original dialogue and when it falls in quiet passages), and restore the original audio 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 final 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 single 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 makes 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 on the final cut; if it still exceeds -1 dBTP, lower by an additional 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_targetThe mode actually used by ffmpeg, the limiter amount, and the measured final-cut values are recorded inassembly_qc.jsonofloudness_modeandloudnorm_final_pass(delivered)。 - The final cut does not carry the original film's container metadata (
title、commentand other tags and chapters).
2. Sound finalization contract
The synthesis stage only implements creative decisions; it does not invent 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 for the sake of "fuller" sound, duck dialogue 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-audio blank space, and existing mixing parameters.
3. Input contract
<video>: source video; in cut mode, it isedited_source.mp4。work_dir/tts_meta.json: defaultnarrationrequired in mode; written by the dubbing stage{segments: [...]}. Each segment containsaudio_path, time,pause_after_ms、overlaps_speechand the positions used for mixing/subtitles. Explicitsource-mix/adopted-packet-copymode does not read it.- Adopted dubbing uses explicit
--tts-metaand--narration-adoption: the latter independently confirms the text, requested engine/voice, and speed policy by the caller, and cannot be automatically "approved" from the metadata to be consumed. For the complete format and recording boundaries, seereferences/narration-adoption.md。 - The adopted complete sound bed and per-segment dubbing can be passed again
--audio-mix-adoption; for the strict format, 48 kHz channel matrix, and dual binding transaction, seereferences/explicit-audio-mix.md。
The following scripts/... are all relative to this skill directory. If the executor is started from the repository root, add this skill's absolute directory to the script path.
4. Run commands
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: final cut within the working directory.subtitles.srt: narration subtitles; when burning in, there is alsosubtitles.ass。timeline.json: Backend-agnostic multitrack model, including video, original audio, narration, BGM, subtitles, and ducking automation._placed_*.wav: The complete segment-by-segment narration PCM actually written into the main mix; the timeline and Jianying only reference these files.narration_input_binding.json: The consumption record of narration input, conversion, actual placement, narration main track, and final audio track (paths, PCM parameters, packet counts). Distinguishes between the two states of not adopted and bound-adopted decision; not equivalent to voice identification or audition.audio_mix_binding.json: The record of the picture clock, base track, 48 kHz voice placement, premaster, fixed master gain, final PCM/AAC facts, and narration binding paths for the explicit complete audio branch consumption.assembly_manifest.json: Input source, cut source identification (path, size, mtime), render settings, and final output path.assembly_qc.json: Release gates for narration integrity, original-audio sentence-end handoff, timeline material duration, and delivery quality.- Jianying 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/fit, original-audio handoff, ambient BGM, duck, loudnorm, and limiter.- Audio is mixed by track: original audio, optional BGM, and narration are each independent.
- The first block of a paragraph (with more than 1.6 seconds of author whitespace from the previous block) is placed from the written
start, but not earlier than the previous block's actual end plus itspause_after_ms; if the previous block times out, it pushes it later; subsequent blocks within the paragraph follow immediately after the previous block's actual end (0.35 second gap), at most 1.2 seconds earlier than the writtenstart. This advance occurs after narration validation, so the advanced segment cannot enter the original-audio dialogue: when there is dialogue, it can advance at most to the end of the last dialogue segment. Blocks that actually advanced recordsource_entry_statusinparagraph_tightened, and record the writtenwritten_startinstart。 - Narration does not undergo any tolerance tail trimming; if it still does not fit after gentle speeding 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 with adopted voiceover supports only original speed and prohibits in-segment speed fitting; the environment default of 1.15x speed or an 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 publish a candidate film. The old entry point without an adopted file is still compatibility mode and does not automatically obtain equivalent evidence.
- The original audio 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, fade in only within the measured pause, and return to full atsource_restore_at(when the anchor isunverified, the state is recorded assentence_boundary_unverified). If there is no anchor within 3 seconds, when less than 3 seconds remain to the timeline end, duck to the end (held_to_timeline_end), otherwise return to full directly at the end of the narration (bounded_release), and do not hold the original-audio dialogue down 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 that the draft references the real original-film intervals.- Jianying 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 onto numbered tracks; a non-empty target directory is not overwritten, but a numbered sibling directory is created instead.
- Export only accepts
schema_version: 2oftimeline.json, mapping only video, audio, subtitles, and image overlays (scale/position); handwritten extension fields such as speed changes, transitions, masks, rich text, and effect tracks are explicitly rejected. - Jianying drafts reference unbaked source video, so hardcoded 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_CHARS, etc. SUBTITLE_Y_TOP/BOTplaces the ASS baseline into the measured subtitle region of the original footage, with coordinates as a half-open[top, bot)on the display canvas, accepting only square or near-square pixels (SAR differing from 1:1 by no more than 2%; unlabeled0:1is treated as square); under the explicit mask strategy, the defaultSUBTITLE_MASK_OPACITY=0.6,SOURCE_SUBTITLE_MASK_TIMING=narration。- Original audio returns to
IDLE_ORIG_VOLUMEduring narration gaps, and narration is ducked toSPEECH_DUCKING_VOLUME;DUCK_FADE_SECONDScontrols the transition. You can also configureDUCK_BRIDGE_SECONDS、ZONE_DUCKING_VOLUME、FINAL_LOUDNORM、TARGET_LUFSandLOUDNESS_LIMITER_MAX_DB。 - You can specify BGM via
BGM_PATH; it loops to the final video 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, and the compositing stage pre-checks before rendering. When burning is explicitly required (--burn-subtitlesor environment variableBURN_SUBTITLES), missing libass fails directly; when it is merely enabled by default, it degrades: no burning, delivering external.srt(the「」original dialogue in the blank space is 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 hardcoded subtitles remain visible),visual_qc.jsonofwarningsandassembly_manifest.jsonofwarningsrecords asubtitle_burn_degraded,subtitles.burn_degraded_reasonwriting the reason. After degradation, the mask is turned off as before (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 final video picture is always H.264 8-bit 4:2:0 with
+faststart: stream copy is used only when no filters are needed, the source picture is already H.264 8-bit 4:2:0 (yuv420p, or full-rangeyuvj420p), and the width and height are even, delivering it as-is; otherwise it is re-encoded toyuv420p. Color tagging: when the source is untagged or already BT.709, it is tagged as BT.709 (-colorspace/-color_primaries/-color_trc bt709); other declared color spaces are preserved as-is; full-range (pc) sources preservepc, and the rest are taggedtv. YUV sources only have their tags changed, without pixel conversion; RGB sources (ffprobe reportsgbr, such as MOV wrapped in PNG/QuickTime RLE) 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)。 - Dialogue subtitles in the original-audio blank space preferentially read the Agent-proofread
original_subtitles.json; otherwise 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 using「」.
Prepare the sound according to the original footage intervals, rather than lowering the entire old final video
When there are already multiple original-audio trade-off and independent BGM decisions, first use the independent
of references/source-score.md independent
source_score.py Reconstruct the original audio track, music track, and the sum of both from the original film's audio stream according to precise frame intervals; it outputs only the
sound base track and a source receipt. To synthesize with the dubbing already adopted segment by segment, the caller must then provide references/explicit-audio-mix.md
strict adoption; do not stuff the base track into the old entry point for automatic ducking, and do not take the entire audio from a finished film containing old narration and pass it off as clean original audio.
7. Subtitles and optional packaging
First lock the picture, cut points, narration, and mix, then invest in subtitle animation or border packaging; subtitle styling must not mask problems with the narrative, cut points, or sound.
For ordinary delivery, prefer the existing ASS path; for full-film static frames, title bars, and corner marks, use packaging_layers.json static overlay;
only when the user needs per-cue layout, animation, or transparent layers should a project-level code renderer be used, with that project compositing the transparent layer onto the locked master itself.
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, and 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 explicitly outputting an axis subtitle track, the complete replacement semantics, and the current boundaries, see references/subtitle-track.md。