@oh-story/dsh 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/lib/client.js +85 -35
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +41 -36
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
- package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
- package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
- package/lib/index.js +16 -7
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/README.md +98 -44
- package/lib/novel-to-game/README_ZH.md +100 -46
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
- package/lib/novel-to-game/manifest.json +303 -78
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
- package/lib/types/client/drama-production-view.d.ts +11 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +1 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +7 -2
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +108 -1
- package/lib/video-recap/README.en.md +18 -14
- package/lib/video-recap/README.md +18 -14
- package/lib/video-recap/manifest.json +297 -321
- package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
- package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
- package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
- package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
- package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
- package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
- package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
- package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
- package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
- package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
- package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
- package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
- package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
- package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
- package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
- package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
- package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
- package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
- package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
- package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
- package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
- package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
- package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
- package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
- package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
- package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
- package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
- package/lib/video-recap/skills/video-script/SKILL.md +9 -7
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
- package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
- package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
- package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
- package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
- package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
- package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
- package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
- package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
- package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
- package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
- package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
- package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
- package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
- package/package.json +45 -45
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
- /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
"""Render edited source media
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
1
|
+
"""Render edited source media."""
|
|
5
2
|
|
|
3
|
+
from fractions import Fraction
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
|
|
6
|
+
from frame_grid import frame_count, parse_frame_rate
|
|
8
7
|
from lib import CONFIG, filter_file_args, get_video_duration, log, run_cmd
|
|
9
8
|
|
|
10
9
|
from cut_contract import _write_edited_source_meta
|
|
11
|
-
from media_geometry import
|
|
12
|
-
|
|
10
|
+
from media_geometry import (
|
|
11
|
+
_color_tag_args,
|
|
12
|
+
_color_tag_filter,
|
|
13
|
+
_has_audio_stream,
|
|
14
|
+
_output_color_tags,
|
|
15
|
+
_probe_video_format,
|
|
16
|
+
)
|
|
17
|
+
from sentence_gate import _continuous_source_join
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
def _audio_segment_filter(
|
|
@@ -18,7 +23,7 @@ def _audio_segment_filter(
|
|
|
18
23
|
max_fade = duration / 2
|
|
19
24
|
fade_in = max(0.0, min(fade_in_ms / 1000.0, max_fade))
|
|
20
25
|
fade_out = max(0.0, min(fade_out_ms / 1000.0, max_fade))
|
|
21
|
-
base = f"{label_in}atrim=start={start:.
|
|
26
|
+
base = f"{label_in}atrim=start={start:.6f}:end={end:.6f},asetpts=PTS-STARTPTS"
|
|
22
27
|
if fade_in > 0:
|
|
23
28
|
base += f",afade=t=in:st=0:d={fade_in:.3f}"
|
|
24
29
|
if fade_out > 0:
|
|
@@ -43,89 +48,95 @@ def _clip_audio_edge_fades(clips, idx, fade_ms):
|
|
|
43
48
|
return fade_in, fade_out
|
|
44
49
|
|
|
45
50
|
|
|
46
|
-
def
|
|
47
|
-
"""
|
|
51
|
+
def _video_segment_filter(label_in, label_out, start, end, frames, out_rate, source_rate, norm=""):
|
|
52
|
+
"""Exactly `frames` frames at `out_rate` from source [start, end).
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
The trim points sit half a source frame early so the frame ON a grid edge is selected
|
|
55
|
+
despite millisecond rounding or container pts jitter. fps resamples onto the output
|
|
56
|
+
grid; tpad clones the last frame without limit, covering a segment that came up short
|
|
57
|
+
(one frame from resampling, or many when the clip runs past the end of a video stream
|
|
58
|
+
shorter than its audio); and the final trim cuts to the exact count, so every segment
|
|
59
|
+
advances the concat by whole frames and edited_source.mp4 stays constant frame rate.
|
|
51
60
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"-
|
|
55
|
-
"
|
|
56
|
-
"-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
half = 0.5 / source_rate if source_rate else 0.0
|
|
62
|
+
return (
|
|
63
|
+
f"{label_in}trim=start={max(0.0, start - half):.6f}:end={end - half:.6f},"
|
|
64
|
+
f"setpts=PTS-STARTPTS,{norm}fps={out_rate},tpad=stop_mode=clone:stop=-1,"
|
|
65
|
+
f"trim=end_frame={frames},setpts=PTS-STARTPTS{label_out}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _warn_on_frame_count_mismatch(path, expected):
|
|
70
|
+
"""Log when the encoded video holds a different frame count than qc.frame_grid records.
|
|
71
|
+
|
|
72
|
+
The exact count rests on fps/tpad/trim behaviour verified on ffmpeg 8 and 9; an older
|
|
73
|
+
ffmpeg that behaves differently would otherwise leave a short or VFR file unnoticed.
|
|
74
|
+
A warning, not a block: final_qc owns blockers.
|
|
75
|
+
"""
|
|
76
|
+
result = run_cmd([
|
|
77
|
+
"ffprobe", "-v", "error", "-select_streams", "v:0", "-count_packets",
|
|
78
|
+
"-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", str(path),
|
|
79
|
+
])
|
|
64
80
|
try:
|
|
65
|
-
|
|
66
|
-
except
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
rendered = int(str(result.stdout).strip())
|
|
82
|
+
except (AttributeError, ValueError):
|
|
83
|
+
return
|
|
84
|
+
if rendered != expected:
|
|
85
|
+
log(f"警告: {path.name} 渲染出 {rendered} 帧,"
|
|
86
|
+
f"qc.frame_grid.frame_count 记录的是 {expected} 帧;"
|
|
87
|
+
"当前 ffmpeg 的 fps/tpad/trim 行为可能不同,请检查帧率是否恒定")
|
|
72
88
|
|
|
73
89
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
if len(source_paths) > 1:
|
|
77
|
-
reasons.append("multi_source_geometry_audio_normalization")
|
|
78
|
-
if any("source_path" not in clip for clip in clips):
|
|
79
|
-
reasons.append("single_source_filter_concat_no_stream_copy")
|
|
80
|
-
return "+".join(reasons)
|
|
90
|
+
def _edited_source_color_tags(source_formats, *, rgb_converted_per_clip=False):
|
|
91
|
+
"""One set of colour tags for the concatenated picture, from each source's probe.
|
|
81
92
|
|
|
93
|
+
Sources that agree keep their shared tags; sources that disagree cannot be described by
|
|
94
|
+
one label, so the picture is BT.709 limited, the same default an untagged source gets,
|
|
95
|
+
and `_clip_color_filter` converts every clip into it. With `rgb_converted_per_clip` the
|
|
96
|
+
RGB sources (by colour space or pixel format) are converted to BT.709 limited yuv420p
|
|
97
|
+
clip by clip, so their tags lose `from_rgb` and match a BT.709 limited YUV source.
|
|
98
|
+
"""
|
|
99
|
+
tags = [_output_color_tags(fmt) for fmt in source_formats]
|
|
100
|
+
if rgb_converted_per_clip:
|
|
101
|
+
tags = [{k: v for k, v in item.items() if k != "from_rgb"} for item in tags]
|
|
102
|
+
if all(item == tags[0] for item in tags):
|
|
103
|
+
return tags[0]
|
|
104
|
+
log("剪辑源视频: 各来源色彩标记不一致,各段统一转换为 BT.709 limited")
|
|
105
|
+
return _output_color_tags({})
|
|
82
106
|
|
|
83
|
-
def update_delivery_qc(validated_plan, *, source_paths, output_path=None, rendered=False):
|
|
84
|
-
"""Attach cut delivery facts to qc.delivery_qc without writing visual_qc."""
|
|
85
|
-
qc = validated_plan["qc"]
|
|
86
|
-
clips = validated_plan["clips"]
|
|
87
|
-
target_sample_rate = 48000
|
|
88
|
-
probed_sample_rate = (
|
|
89
|
-
_probe_audio_sample_rate(output_path)
|
|
90
|
-
if output_path and Path(output_path).exists()
|
|
91
|
-
else None
|
|
92
|
-
)
|
|
93
|
-
delivery_qc = {
|
|
94
|
-
"schema_version": 1,
|
|
95
|
-
"video_encode_passes": 1,
|
|
96
|
-
"reencode_reason": _delivery_reencode_reason(source_paths, clips),
|
|
97
|
-
"stream_copy_risk": {
|
|
98
|
-
"status": "avoided",
|
|
99
|
-
"reason": "cut uses trim/concat/filtergraph with explicit libx264/aac encode; no risky stream-copy path",
|
|
100
|
-
},
|
|
101
|
-
"audio_sample_rate": {
|
|
102
|
-
"target": target_sample_rate,
|
|
103
|
-
"probed": probed_sample_rate,
|
|
104
|
-
},
|
|
105
|
-
"final_compat_notes": [
|
|
106
|
-
"video encoded with libx264/yuv420p-compatible filter path",
|
|
107
|
-
"audio encoded as AAC with 48000 Hz target for delivery compatibility",
|
|
108
|
-
"edited_source.mp4 is an intermediate; downstream assembly may perform another intentional encode",
|
|
109
|
-
],
|
|
110
|
-
"output_geometry": qc["output_geometry"],
|
|
111
|
-
"rendered": rendered,
|
|
112
|
-
"planned": not rendered,
|
|
113
|
-
}
|
|
114
|
-
if probed_sample_rate and probed_sample_rate != target_sample_rate:
|
|
115
|
-
delivery_qc["final_compat_notes"].append(
|
|
116
|
-
f"probed audio sample rate {probed_sample_rate} differs from target {target_sample_rate}"
|
|
117
|
-
)
|
|
118
|
-
qc["delivery_qc"] = delivery_qc
|
|
119
|
-
return delivery_qc
|
|
120
107
|
|
|
108
|
+
# ffprobe colour-space names the scale filter can convert from/to (in/out_color_matrix).
|
|
109
|
+
_SCALE_COLOR_MATRIX = {
|
|
110
|
+
"bt709": "bt709", "bt470bg": "bt470", "smpte170m": "smpte170m", "fcc": "fcc",
|
|
111
|
+
"smpte240m": "smpte240m", "bt2020nc": "bt2020",
|
|
112
|
+
}
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
|
|
115
|
+
def _clip_color_filter(source_format, target_tags):
|
|
116
|
+
"""Per-clip conversion of one source's picture into the shared yuv420p picture.
|
|
117
|
+
|
|
118
|
+
The conversion is explicit for every source: the concat filter needs one pixel format,
|
|
119
|
+
and ffmpeg 8 also negotiates one colour space and range across its inputs, silently
|
|
120
|
+
converting the others with its own pick while the stamped label says something else.
|
|
121
|
+
An RGB source converts with the target's matrix; a YUV source converts from its own
|
|
122
|
+
matrix and range (untagged counts as BT.709, the label it would get alone). Only the
|
|
123
|
+
matrix and range are converted; primaries and transfer are relabelled, not remapped.
|
|
124
|
+
"""
|
|
125
|
+
tags = _output_color_tags(source_format)
|
|
126
|
+
out_matrix = _SCALE_COLOR_MATRIX.get(target_tags.get("colorspace"))
|
|
127
|
+
out_range = target_tags["color_range"]
|
|
128
|
+
if tags.get("from_rgb"):
|
|
129
|
+
return f"scale=out_color_matrix={out_matrix or 'bt709'}:out_range={out_range},format=yuv420p,"
|
|
130
|
+
in_matrix = _SCALE_COLOR_MATRIX.get(tags.get("colorspace"))
|
|
131
|
+
if in_matrix and out_matrix:
|
|
132
|
+
return (
|
|
133
|
+
f"scale=in_color_matrix={in_matrix}:in_range={tags['color_range']}"
|
|
134
|
+
f":out_color_matrix={out_matrix}:out_range={out_range},format=yuv420p,"
|
|
135
|
+
)
|
|
136
|
+
if tags != target_tags:
|
|
137
|
+
log(f"剪辑源视频: 色彩空间 {tags.get('colorspace')} 无法转换为 "
|
|
138
|
+
f"{target_tags.get('colorspace')},该来源只改标记")
|
|
139
|
+
return "format=yuv420p,"
|
|
129
140
|
|
|
130
141
|
|
|
131
142
|
def build_edited_source_video(input_video, validated_plan, work_dir, output_path=None):
|
|
@@ -133,7 +144,8 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
133
144
|
|
|
134
145
|
`validated_plan["qc"]["output_geometry"]` is required: the caller (cut_cli, or any
|
|
135
146
|
public user of this API) selects the canvas with `_select_output_geometry` first, so the
|
|
136
|
-
same geometry is recorded in clip_plan_validated.json and used for the render.
|
|
147
|
+
same geometry is recorded in clip_plan_validated.json and used for the render. Each clip
|
|
148
|
+
renders `frame_count(duration, output frame_rate)` frames and exactly that much audio."""
|
|
137
149
|
work_dir = Path(work_dir)
|
|
138
150
|
output_path = Path(output_path or work_dir / "edited_source.mp4")
|
|
139
151
|
clips = validated_plan["clips"]
|
|
@@ -143,6 +155,11 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
143
155
|
"validated_plan['qc']['output_geometry'] is required: select the canvas with "
|
|
144
156
|
"media_geometry._select_output_geometry before build_edited_source_video"
|
|
145
157
|
)
|
|
158
|
+
geometry_qc = qc["output_geometry"]
|
|
159
|
+
out_rate = Fraction(geometry_qc["frame_rate"])
|
|
160
|
+
source_rates = {
|
|
161
|
+
row["path"]: parse_frame_rate(row.get("frame_rate")) for row in geometry_qc["sources"]
|
|
162
|
+
}
|
|
146
163
|
|
|
147
164
|
source_paths = []
|
|
148
165
|
for clip in clips:
|
|
@@ -151,106 +168,99 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
151
168
|
source_paths.append(source_path)
|
|
152
169
|
source_index = {path: idx for idx, path in enumerate(source_paths)}
|
|
153
170
|
audio_by_input = {path: _has_audio_stream(path) for path in source_paths}
|
|
171
|
+
format_by_input = {path: _probe_video_format(path) for path in source_paths}
|
|
154
172
|
join_fade_ms = CONFIG["clip_join_audio_fade_ms"]
|
|
155
173
|
qc["join_fade_ms"] = round(join_fade_ms, 3)
|
|
156
174
|
|
|
175
|
+
color_tags = _edited_source_color_tags(
|
|
176
|
+
[format_by_input[path] for path in source_paths],
|
|
177
|
+
rgb_converted_per_clip=len(source_paths) > 1,
|
|
178
|
+
)
|
|
157
179
|
parts = []
|
|
158
180
|
concat_inputs = []
|
|
159
181
|
extra_inputs = []
|
|
182
|
+
vnorm_by_input = {}
|
|
160
183
|
if len(source_paths) > 1:
|
|
161
184
|
# Distinct sources almost always differ in resolution/SAR/fps/pixel-format (and
|
|
162
185
|
# some may lack audio), which the bare concat filter rejects. Normalize every video
|
|
163
186
|
# segment to one canvas and give every clip an audio segment (real or synthesized
|
|
164
187
|
# silence) so concat always succeeds with a continuous track and no source's audio
|
|
165
188
|
# is dropped just because a sibling source is silent.
|
|
166
|
-
|
|
167
|
-
canvas_w, canvas_h, canvas_fps = (
|
|
168
|
-
geometry_qc["width"],
|
|
169
|
-
geometry_qc["height"],
|
|
170
|
-
geometry_qc["fps"],
|
|
171
|
-
)
|
|
189
|
+
canvas_w, canvas_h = geometry_qc["width"], geometry_qc["height"]
|
|
172
190
|
vnorm = (
|
|
173
191
|
f"scale={canvas_w}:{canvas_h}:force_original_aspect_ratio=decrease,"
|
|
174
192
|
f"pad={canvas_w}:{canvas_h}:(ow-iw)/2:(oh-ih)/2,setsar=1,"
|
|
175
|
-
f"fps={canvas_fps},format=yuv420p"
|
|
176
193
|
)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
# Each clip becomes yuv420p in the shared colour space and range here, before the
|
|
195
|
+
# shared tags are stamped; left to `format=yuv420p` alone, ffmpeg picks the matrix
|
|
196
|
+
# (BT.601 for RGB) while the file is labelled with the shared tags.
|
|
197
|
+
vnorm_by_input = {
|
|
198
|
+
path: vnorm + _clip_color_filter(fmt, color_tags)
|
|
199
|
+
for path, fmt in format_by_input.items()
|
|
200
|
+
}
|
|
201
|
+
has_audio = len(source_paths) > 1 or all(audio_by_input.values())
|
|
202
|
+
total_frames = 0
|
|
203
|
+
end = None
|
|
204
|
+
for clip_pos, clip in enumerate(clips):
|
|
205
|
+
idx = clip["clip_id"]
|
|
206
|
+
clip_source = clip.get("source_path", str(input_video))
|
|
207
|
+
input_idx = source_index[clip_source]
|
|
208
|
+
# A lossless join starts exactly where the previous clip's render ended, not at the
|
|
209
|
+
# plan's millisecond-rounded copy of it, so the two atrims stay sample-contiguous.
|
|
210
|
+
joined = clip_pos > 0 and _continuous_source_join(clips[clip_pos - 1], clip)
|
|
211
|
+
start = end if joined else clip["source_start"]
|
|
212
|
+
frames = clip.get("frame_count") or frame_count(clip["duration"], out_rate)
|
|
213
|
+
total_frames += frames
|
|
214
|
+
dur = float(frames / out_rate)
|
|
215
|
+
end = start + dur
|
|
216
|
+
parts.append(_video_segment_filter(
|
|
217
|
+
f"[{input_idx}:v]", f"[v{idx}]", start, end, frames, out_rate,
|
|
218
|
+
source_rates.get(clip_source), vnorm_by_input.get(clip_source, ""),
|
|
219
|
+
))
|
|
220
|
+
concat_inputs.append(f"[v{idx}]")
|
|
221
|
+
if not has_audio:
|
|
222
|
+
continue
|
|
223
|
+
if audio_by_input[clip_source]:
|
|
184
224
|
fade_in_ms, fade_out_ms = _clip_audio_edge_fades(clips, clip_pos, join_fade_ms)
|
|
185
225
|
parts.append(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
extra_filters="aresample=48000,aformat=sample_rates=48000:channel_layouts=stereo",
|
|
199
|
-
)
|
|
226
|
+
_audio_segment_filter(
|
|
227
|
+
f"[{input_idx}:a]",
|
|
228
|
+
f"[a{idx}]",
|
|
229
|
+
start,
|
|
230
|
+
end,
|
|
231
|
+
dur,
|
|
232
|
+
fade_in_ms,
|
|
233
|
+
fade_out_ms,
|
|
234
|
+
extra_filters=(
|
|
235
|
+
"aresample=48000,aformat=sample_rates=48000:channel_layouts=stereo"
|
|
236
|
+
if len(source_paths) > 1 else ""
|
|
237
|
+
),
|
|
200
238
|
)
|
|
201
|
-
else:
|
|
202
|
-
parts.append(
|
|
203
|
-
f"anullsrc=r=48000:cl=stereo,atrim=duration={dur:.3f},asetpts=PTS-STARTPTS,"
|
|
204
|
-
f"aformat=sample_rates=48000:channel_layouts=stereo[a{idx}]"
|
|
205
|
-
)
|
|
206
|
-
concat_inputs.append(f"[v{idx}][a{idx}]")
|
|
207
|
-
parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=1[v][a]")
|
|
208
|
-
maps = ["-map", "[v]", "-map", "[a]"]
|
|
209
|
-
else:
|
|
210
|
-
has_audio = all(audio_by_input.values())
|
|
211
|
-
for clip_pos, clip in enumerate(clips):
|
|
212
|
-
idx = clip["clip_id"]
|
|
213
|
-
input_idx = source_index[clip.get("source_path", str(input_video))]
|
|
214
|
-
start = clip["source_start"]
|
|
215
|
-
end = clip["source_end"]
|
|
216
|
-
parts.append(
|
|
217
|
-
f"[{input_idx}:v]trim=start={start:.3f}:end={end:.3f},setpts=PTS-STARTPTS[v{idx}]"
|
|
218
239
|
)
|
|
219
|
-
|
|
220
|
-
if has_audio:
|
|
221
|
-
fade_in_ms, fade_out_ms = _clip_audio_edge_fades(
|
|
222
|
-
clips, clip_pos, join_fade_ms
|
|
223
|
-
)
|
|
224
|
-
parts.append(
|
|
225
|
-
_audio_segment_filter(
|
|
226
|
-
f"[{input_idx}:a]",
|
|
227
|
-
f"[a{idx}]",
|
|
228
|
-
start,
|
|
229
|
-
end,
|
|
230
|
-
end - start,
|
|
231
|
-
fade_in_ms,
|
|
232
|
-
fade_out_ms,
|
|
233
|
-
)
|
|
234
|
-
)
|
|
235
|
-
concat_inputs.append(f"[a{idx}]")
|
|
236
|
-
|
|
237
|
-
if has_audio:
|
|
240
|
+
else:
|
|
238
241
|
parts.append(
|
|
239
|
-
|
|
242
|
+
f"anullsrc=r=48000:cl=stereo,atrim=duration={dur:.6f},asetpts=PTS-STARTPTS,"
|
|
243
|
+
f"aformat=sample_rates=48000:channel_layouts=stereo[a{idx}]"
|
|
240
244
|
)
|
|
241
|
-
|
|
242
|
-
else:
|
|
243
|
-
parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=0[v]")
|
|
244
|
-
maps = ["-map", "[v]", "-map", f"{len(source_paths)}:a", "-shortest"]
|
|
245
|
-
extra_inputs = [
|
|
246
|
-
"-f",
|
|
247
|
-
"lavfi",
|
|
248
|
-
"-t",
|
|
249
|
-
f"{validated_plan['total_duration']:.3f}",
|
|
250
|
-
"-i",
|
|
251
|
-
"anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
252
|
-
]
|
|
245
|
+
concat_inputs.append(f"[a{idx}]")
|
|
253
246
|
|
|
247
|
+
if has_audio:
|
|
248
|
+
parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=1[v][a]")
|
|
249
|
+
maps = ["-map", "[v]", "-map", "[a]"]
|
|
250
|
+
else:
|
|
251
|
+
parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=0[v]")
|
|
252
|
+
maps = ["-map", "[v]", "-map", f"{len(source_paths)}:a", "-shortest"]
|
|
253
|
+
extra_inputs = [
|
|
254
|
+
"-f",
|
|
255
|
+
"lavfi",
|
|
256
|
+
"-t",
|
|
257
|
+
f"{float(total_frames / out_rate):.6f}",
|
|
258
|
+
"-i",
|
|
259
|
+
"anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
parts.append(f"[v]{_color_tag_filter(color_tags)}[vtagged]")
|
|
263
|
+
maps[maps.index("[v]")] = "[vtagged]"
|
|
254
264
|
filter_complex = ";".join(parts)
|
|
255
265
|
if len(filter_complex.encode("utf-8")) > 7000:
|
|
256
266
|
filter_script = work_dir / "edit_filter_complex.txt"
|
|
@@ -285,19 +295,20 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
285
295
|
"48000",
|
|
286
296
|
"-movflags",
|
|
287
297
|
"+faststart",
|
|
298
|
+
# The source's container/stream tags (a scraper's title, comment or URL) and its
|
|
299
|
+
# chapters would otherwise be copied into edited_source.mp4 and on into the recap.
|
|
300
|
+
"-map_metadata",
|
|
301
|
+
"-1",
|
|
302
|
+
"-map_chapters",
|
|
303
|
+
"-1",
|
|
304
|
+
*_color_tag_args(color_tags),
|
|
288
305
|
str(output_path),
|
|
289
306
|
]
|
|
290
307
|
result = run_cmd(cmd)
|
|
291
308
|
if result.returncode != 0:
|
|
292
309
|
raise RuntimeError(f"剪辑源视频失败: {result.stderr}")
|
|
293
310
|
|
|
294
|
-
|
|
295
|
-
validated_plan,
|
|
296
|
-
source_paths=source_paths,
|
|
297
|
-
output_path=output_path,
|
|
298
|
-
rendered=True,
|
|
299
|
-
)
|
|
300
|
-
write_cut_delivery_qc(work_dir, validated_plan)
|
|
311
|
+
_warn_on_frame_count_mismatch(output_path, total_frames)
|
|
301
312
|
_write_edited_source_meta(output_path, validated_plan, input_video)
|
|
302
313
|
duration = get_video_duration(output_path)
|
|
303
314
|
log(f"剪辑源视频: {output_path} ({duration:.1f}s, {len(clips)} clips)")
|