1. Positioning
This skill only executes editing decisions the Agent has already made:
- Validate and complete
clip_plan.json, write out withclip_id, the original/output time and duration ofclip_plan_validated.json。 - First avoid the risk of flash frames near hard cuts in the original video, then snap boundaries to reliable sentence ends/natural pauses; audio integrity has final priority.
- Align each in-point to the source video frame grid, and each segment duration to an integer number of output frames (for a move of less than one frame, prefer the side that the sentence-boundary gate still judges safe and that is still within the pause; when both sides are equally safe, choose the side that does not cross a hard cut in the original video, to avoid a one-frame flash). The sentence-boundary gate checks the aligned boundaries.
- Splice the selected intervals and output at constant frame rate
edited_source.mp4, with the frame count matching what is recorded inclip_plan_validated.json. - Stop here; the Agent writes
narration.jsonaccording to the real output timeline; this tool does not read narration, nor does it do original-to-output mapping.
The same input will produce the same output.edited_source.mp4.meta.json Record normalized clips, render settings, and each source file's size/mtime_ns; reuse only if all three match the current ones and edited_source.mp4 exists and is non-empty; re-render if any differs. If there is only a sidecar but no media file, do not reuse.
2. Input Contract
work_dir/clip_plan.json can be an array, or {"clips": [...]}:
{"start": 12.0, "end": 28.5, "reason": "b02 | turn | power: A→B | POV=女主 | 保留反应 | 入点=问题落下 | 出点=沉默结束"}
start/endis in original seconds; also acceptssource_start/source_endorin/out。- top-level optional
target_duration, e.g."10m"。 - Each clip in a multi-video project must also fill in
source_id(does not acceptidas a substitute), and pass in the source manifest via--sources-manifest(shape see below). speech_boundary_anchors.jsonand ASR time ranges are provided by the understanding stage; the Agent first writes rough intervals, and the tool will try to snap them and return in/out points still within speech intervals as blockers. Windows containing only interjections or ASR noise ("Ah!", "Hi.") do not count as speech intervals; keep only a 1-second guard on the side immediately adjacent to real dialogue. Windows containing only punctuation ("...") still count as speech.
The multi-video source manifest accepts only one shape; other shapes directly error out and state the expected shape:
{"sources": [{"source_id": "ep1", "source_path": "/media/ep1.mp4", "duration": 1520.0, "source_work_dir": "sources/ep1"}]}
duration may be omitted (when omitted, read with ffprobe);source_work_dir may be omitted; when filled in, it is relative to --work-dir, used to read that source's silence, sentence-end anchors, and ASR. Other keys are ignored.
3. Editing Intent Contract
The tool will not make creative choices for the Agent. Before writing clips, complete the editing intent check in this section, and map each interval to recap_story_plan.json one beat of .
Use the existing free-text reason to save concise decisions:
beat_id | function | change | POV | preferred moment | 入点 reason | 出点 reason
Do not keep the entire segment just because an "event is important"; keep the specific performance, reaction, action, or reveal that best makes the change hold. When understanding and emotion allow, enter late and exit early, while ensuring dialogue, action, and technical boundaries are intact.
For Q&A, reactions, or action payoffs that cannot be deleted, first verify the source evidence, then register the exact interval in the same clip_plan.json :
{
"clips": [{"start": 12, "end": 18}],
"required_evidence": {
"nodes": [
{"id": "refusal", "source": "/media/episode.mp4", "start": 12.25, "end": 14.5, "track": "audio", "content": "对方拒绝请求"},
{"id": "response", "source": "/media/episode.mp4", "start": 15, "end": 17.5, "track": "video", "content": "听到拒绝后的反应与决定"}
],
"before": [["refusal", "response"]]
}
}
source uses the actual source file absolute path,start/end is in original seconds; for multiple sources, optionally fill in source_id to disambiguate. Only register specific moments that truly need to be kept; do not lock the entire beat by default.before Only record the precedence relationships required by this clip; when no order constraint is needed, write before: []。
After snapping all frames/sentence boundaries, the tool checks that every must-preserve moment has at least one fully continuous preserved instance, source, and precedence; for audio nodes it also checks whether the source audio track exists. Every result occurrence (including partial fragments) must satisfy its declared premise; a later complete segment cannot be used to let a fragment that lacks a prerequisite at the beginning pass. Write the result to clip_plan_validated.json.qc.required_evidence; missing segments, wrong order, or invalid declarations will be blocked during precheck, cache reuse, and before rendering, and the duration relaxation option will not skip them. This result verifies that the selected segments are preserved; the actual semantics and final mix are still checked according to the review steps.
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 paths.
4. Run the command
python3 scripts/cut.py <video> --work-dir <work_dir> [--clip-plan <clip_plan.json>] \
[--sources-manifest <sources.json>] [--target-duration 10m] [--allow-overlap] \
[--allow-duration-drift] [--normalize-only] \
[--review-shots [--shot-scene-threshold 0.35] [--shot-roi X Y W H]]
-
--clip-plan: clip plan path, default<work_dir>/clip_plan.json。 -
--sources-manifest: source list for multi-source editing{"sources": [{"source_id", "source_path"[, "duration", "source_work_dir"]}]}; for fragments usesource_idto specify the source, and snap sentence boundaries and frame cut points according to its own source. -
--target-duration: target duration. If the ratio of actual duration to target is outside 0.85–1.15, a warning is recorded; outside 0.60–1.40, it is blocked. -
--allow-duration-drift: only allow duration deviation blocking (recorded asallowed: truewarning), do not allow sentence-boundary or must-preserve evidence blocking. -
--normalize-only: only normalize, snap, and check the plan, write outclip_plan_validated.jsonand then exit without rendering. -
--review-shots: on the rendered or reusededited_source.mp4, recall short-shot and dense-cut candidates, report only and do not fix;--shot-scene-thresholdis the recall threshold (default 0.35, not an acceptance criterion),--shot-roiscans only that pixel rectangle (use when there are black bars or packaging), and does not crop the picture.
When cut is blocked, exit with a non-zero status and write the reason to clip_plan_validated.json 's qc.blocking, each item with code:
unsafe_clip_sentence_boundary: the fragment boundary is still inside the original spoken audio; seeqc.boundary_status.sentence_checksfor per-boundary determination. A blocked boundary hasnearest_safe: {"before", "after"}: the nearest safe boundary within 5 seconds before and after{time, reason, delta}(original clip seconds;deltais the number of seconds relative to the current boundary, ornullif none; it is already a landing point on the original frame grid and has been rechecked; when rerunning, if shot-change avoidance pulls it back into speech, this avoidance will be reverted,qc.boundary_status.shot_snapsrecordsreverted_unsafe, so writing it back as-is will no longer be blocked by the sentence boundary), modify according to itclip_plan.json'sstart/endand rerun. Moving the in-point earlier (before) keeps more, moving it later (after) trims it, and the out-point is the opposite; first confirm that the change will not cut must-preserve content or overlap adjacent fragments. Both sides beingnullmeans there is no pause nearby, so change the interval instead of fine-tuning.target_duration_drift: duration deviation exceeds the blocking threshold; details inqc.target_duration。REQUIRED_EVIDENCE_INVALID/REQUIRED_EVIDENCE_MISSING/REQUIRED_EVIDENCE_ORDER/REQUIRED_EVIDENCE_AUDIO_UNAVAILABLE: must-preserve evidence declaration is invalid, missing segments, wrong order, or source has no audio track; details inqc.required_evidence。
5. Output contract
clip_plan_validated.json: normalized fragments, includingclip_id、source_start/end、output_start/end、durationandframe_count(the number of frames rendered for that fragment).qc.frame_gridrecords output frame rateoutput_frame_rate(single-source uses the source frame rate, multi-source uses the canvas frame rate, NTSC is written as30000/1001), total frame count, and each source frame rate; the before/after time of each frame alignment is written inqc.boundary_status.frame_snaps。edited_source.mp4: a constant-frame-rate short video after splicing according to the plan, with frame count equal toqc.frame_grid.frame_count。shot_review.json: only the actual video short-shot/dense-cut candidates generated after--review-shotsis enabled; it will not change the plan.
Downstream treats edited_source.mp4 Treated as video, write the Agent according to the output time narration.json as narration.
6. Boundary and Timeline Rules
clip_plan.jsonUse the original clip time;narration.jsondirectly use the post-cut output time; there is no original clip → output narration mapping. The output time is based onclip_plan_validated.jsonas the standard: frame alignment will move boundaries by less than one frame (no more than 40 ms at 25fps); before writing narration, read the validated plan, and do not estimate using the original intervals you wrote yourself.- When the boundary is not on the frame grid, concat will drop one frame slot at each junction (at 25fps the picture pauses for 80 ms, and the final video becomes variable frame rate), so frame alignment cannot be turned off. When the source frame rate is unknown (
r_frame_rateis0/0or greater than 120), the in-point does not move, and the duration is still aligned to an integer number of output frames. - By default, overlapping or repeating original clip intervals is prohibited;
--allow-overlaponly allowed after it is enabled. - A segment start point can only be located at the source start, a reliable sentence end/silence window, or form a lossless same-source continuous connection with the previous segment; the same applies to segment end points. If ASR determines that speech is still ongoing and it cannot snap, write
unsafe_clip_sentence_boundaryand block. SCENE_CUT_SNAPEnabled by default: first snap source start backward and source end forward to nearby hard cuts based on the picture, then perform final correction with sentence-end snapping, to avoid visual correction re-creating half-sentence original audio; when there is no nearby pause and sentence-end snapping cannot correct it, the avoidance that moved the boundary into speech will be withdrawn (if the original position can pass the gate). Default range isSCENE_CUT_SNAP_MARGIN=0.5seconds, detection threshold isSCENE_CUT_DETECT_THRESHOLD=0.4。- The scene-change score only provides junction candidates; it does not prove that a junction is natural. First check whether dense candidates appear within a short time window, then distinguish the source: unrelated short shots from the original clip should be deleted entirely; related but short shots that look like flash frames should be preserved by expanding IN/OUT to keep the complete action, reaction, or dialogue, without using freeze frame/slow motion to fake duration; cut points created by this concatenation should preferably be handled by moving the boundary, restoring same-source continuous motion, merging adjacent segments, or switching to a more natural connection, to eliminate them as much as possible. After the final video is produced, still play around each junction for about 0.5–1 second one by one; for white flashes or exposure dissolves, combine with frame-by-frame brightness localization; do not cut off complete dialogue just to pass visual detection, and do not use transitions to cover bad junctions.
- When trimming short residual shots, do not apply extreme zoom or displacement near the junction that is discontinuous with the shot it belongs to just to lower the scene score; for the framing review and repair verification workflow, see
references/shot-review.md。 - For lossless connections of continuous same-source segments, do not apply double-sided audio fade-out within the sentence; for non-continuous segments, still apply anti-pop fade-in/fade-out within safe pauses.
When it is necessary to check frequent shot changes within a short time, first use the ffmpeg scene filter to recall candidate times:
ffmpeg -i input.mp4 -vf "select='gt(scene,0.35)',showinfo" -an -f null -
0.35 is the starting threshold, not a quality criterion; large motion, white flashes, and dissolves may all cause false positives. After mapping candidates back to the original clip shot and this concatenation boundary, handle them according to the source classification above, and play at normal speed to decide whether to keep them.
When it is necessary to be precise to the actual frame, inspect residual shots inside long intervals, and record the plan path used, use
scripts/shot_review.py or cut.py --review-shots(add --roi / --shot-roiwhen there are black bars or packaging);
see references/shot-review.md。
7. Capability Boundaries
- Does not perform semantic understanding, does not write narration, and does not choose segments for the Agent; the scene filter only handles technical boundary candidate detection.
- Only performs generation
edited_source.mp4the required cutting, concatenation, and one intermediate encoding, and does not handle subtitle packaging or final delivery compression.