@oh-story/dsh 0.1.7 → 0.1.9
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 +30 -15
- package/lib/client.js +93 -16
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +165 -69
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
- package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/manifest.json +28 -28
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
- package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
- package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
- package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
- package/lib/oh-story/manifest.json +292 -172
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +56 -242
- package/lib/oh-story/roles/story-architect.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +1 -1
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/SKILL.md +13 -3
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +17 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +41 -41
|
@@ -111,9 +111,7 @@ def main():
|
|
|
111
111
|
# implements padding — and it used to read the CLI flag alone, so setting the env var did
|
|
112
112
|
# nothing at all while `clip_padding_source: "env"` reported otherwise. CLI still wins.
|
|
113
113
|
clip_padding = (
|
|
114
|
-
args.clip_padding
|
|
115
|
-
if args.clip_padding is not None
|
|
116
|
-
else float(CONFIG.get("clip_padding", 0.0) or 0.0)
|
|
114
|
+
args.clip_padding if args.clip_padding is not None else CONFIG["clip_padding"]
|
|
117
115
|
)
|
|
118
116
|
|
|
119
117
|
work_dir = Path(args.work_dir)
|
|
@@ -153,34 +151,32 @@ def main():
|
|
|
153
151
|
# Keep boundaries off the original footage's hard cuts (avoids 闪烁 at the edit point).
|
|
154
152
|
# This visual-only pass runs FIRST. The sentence/quiet pass below is the final authority:
|
|
155
153
|
# a prettier edit point must never move the final boundary back inside a spoken sentence.
|
|
156
|
-
if sources_manifest is None and CONFIG
|
|
154
|
+
if sources_manifest is None and CONFIG["scene_cut_snap"]:
|
|
157
155
|
validated_plan = snap_clips_off_shot_changes(
|
|
158
156
|
validated_plan,
|
|
159
157
|
args.video,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
threshold=CONFIG.get("scene_cut_detect_threshold", 0.4),
|
|
158
|
+
margin=CONFIG["scene_cut_snap_margin"],
|
|
159
|
+
threshold=CONFIG["scene_cut_detect_threshold"],
|
|
163
160
|
)
|
|
164
161
|
|
|
165
162
|
if sources_manifest is None:
|
|
166
|
-
silence_periods = _load_silence_for_source(work_dir, None)
|
|
167
|
-
sentence_boundaries = _load_sentence_boundary_windows(work_dir)
|
|
168
163
|
safe_boundaries = _combine_boundary_windows(
|
|
169
|
-
|
|
164
|
+
_load_silence_for_source(work_dir, None),
|
|
165
|
+
_load_sentence_boundary_windows(work_dir),
|
|
170
166
|
)
|
|
171
|
-
if CONFIG
|
|
167
|
+
if CONFIG["snap_clip_line_end"]:
|
|
172
168
|
validated_plan = snap_clip_starts_to_lines(
|
|
173
169
|
validated_plan,
|
|
174
170
|
safe_boundaries,
|
|
175
171
|
video_duration,
|
|
176
|
-
CONFIG
|
|
177
|
-
max_trim=CONFIG
|
|
172
|
+
CONFIG["clip_start_snap_max_prepend"],
|
|
173
|
+
max_trim=CONFIG["clip_start_snap_max_trim"],
|
|
178
174
|
)
|
|
179
175
|
validated_plan = snap_clip_ends_to_lines(
|
|
180
176
|
validated_plan,
|
|
181
177
|
safe_boundaries,
|
|
182
178
|
video_duration,
|
|
183
|
-
CONFIG
|
|
179
|
+
CONFIG["clip_snap_max_extend"],
|
|
184
180
|
)
|
|
185
181
|
validated_plan = enforce_clip_sentence_boundaries(
|
|
186
182
|
validated_plan,
|
|
@@ -194,54 +190,50 @@ def main():
|
|
|
194
190
|
if sources_manifest is not None:
|
|
195
191
|
validated_plan = snap_multi_source_clips(
|
|
196
192
|
validated_plan,
|
|
197
|
-
validated_plan
|
|
193
|
+
validated_plan["sources"],
|
|
198
194
|
work_dir,
|
|
199
|
-
line_max_extend=CONFIG
|
|
200
|
-
scene_margin=CONFIG
|
|
201
|
-
scene_threshold=CONFIG
|
|
202
|
-
do_line_snap=CONFIG
|
|
203
|
-
do_scene_snap=CONFIG
|
|
204
|
-
start_max_prepend=CONFIG
|
|
205
|
-
start_max_trim=CONFIG
|
|
195
|
+
line_max_extend=CONFIG["clip_snap_max_extend"],
|
|
196
|
+
scene_margin=CONFIG["scene_cut_snap_margin"],
|
|
197
|
+
scene_threshold=CONFIG["scene_cut_detect_threshold"],
|
|
198
|
+
do_line_snap=CONFIG["snap_clip_line_end"],
|
|
199
|
+
do_scene_snap=CONFIG["scene_cut_snap"],
|
|
200
|
+
start_max_prepend=CONFIG["clip_start_snap_max_prepend"],
|
|
201
|
+
start_max_trim=CONFIG["clip_start_snap_max_trim"],
|
|
206
202
|
)
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
source_path
|
|
216
|
-
if source_path and source_path not in source_paths:
|
|
217
|
-
source_paths.append(source_path)
|
|
218
|
-
if not source_paths:
|
|
219
|
-
source_paths = [str(args.video)]
|
|
220
|
-
_, _, _, geometry_qc = _select_output_geometry(
|
|
221
|
-
source_paths, validated_plan.get("clips", [])
|
|
222
|
-
)
|
|
223
|
-
validated_plan["qc"]["output_geometry"] = geometry_qc
|
|
224
|
-
validated_plan["qc"]["output_geometry_reason"] = geometry_qc.get("reason")
|
|
225
|
-
allow_duration_drift = bool(args.allow_duration_drift or args.allow_sparse_cut)
|
|
226
|
-
drift_source = (
|
|
227
|
-
"--allow-duration-drift"
|
|
228
|
-
if args.allow_duration_drift
|
|
229
|
-
else ("--allow-sparse-cut" if args.allow_sparse_cut else None)
|
|
230
|
-
)
|
|
231
|
-
update_cut_qc(
|
|
232
|
-
validated_plan,
|
|
233
|
-
allow_duration_drift=allow_duration_drift,
|
|
234
|
-
duration_drift_allowed_by=drift_source,
|
|
235
|
-
)
|
|
236
|
-
update_delivery_qc(
|
|
237
|
-
validated_plan,
|
|
238
|
-
source_paths=source_paths,
|
|
239
|
-
output_path=work_dir / "edited_source.mp4",
|
|
204
|
+
validated_plan["raw_plan_fingerprint"] = value_fingerprint(raw_plan)
|
|
205
|
+
validated_plan.setdefault("qc", {})["join_fade_ms"] = round(
|
|
206
|
+
CONFIG["clip_join_audio_fade_ms"], 3
|
|
207
|
+
)
|
|
208
|
+
# Single-source clips carry no source_path; the CLI video is the only input.
|
|
209
|
+
source_paths = list(
|
|
210
|
+
dict.fromkeys(
|
|
211
|
+
clip["source_path"] for clip in validated_plan["clips"] if "source_path" in clip
|
|
240
212
|
)
|
|
213
|
+
) or [str(args.video)]
|
|
214
|
+
_, _, _, geometry_qc = _select_output_geometry(source_paths, validated_plan["clips"])
|
|
215
|
+
validated_plan["qc"]["output_geometry"] = geometry_qc
|
|
216
|
+
validated_plan["qc"]["output_geometry_reason"] = geometry_qc["reason"]
|
|
217
|
+
allow_duration_drift = bool(args.allow_duration_drift or args.allow_sparse_cut)
|
|
218
|
+
drift_source = (
|
|
219
|
+
"--allow-duration-drift"
|
|
220
|
+
if args.allow_duration_drift
|
|
221
|
+
else ("--allow-sparse-cut" if args.allow_sparse_cut else None)
|
|
222
|
+
)
|
|
223
|
+
update_cut_qc(
|
|
224
|
+
validated_plan,
|
|
225
|
+
allow_duration_drift=allow_duration_drift,
|
|
226
|
+
duration_drift_allowed_by=drift_source,
|
|
227
|
+
)
|
|
228
|
+
update_delivery_qc(
|
|
229
|
+
validated_plan,
|
|
230
|
+
source_paths=source_paths,
|
|
231
|
+
output_path=work_dir / "edited_source.mp4",
|
|
232
|
+
)
|
|
241
233
|
(work_dir / "clip_plan_validated.json").write_text(
|
|
242
234
|
json.dumps(validated_plan, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
243
235
|
)
|
|
244
|
-
if
|
|
236
|
+
if validated_plan["qc"].get("blocking"):
|
|
245
237
|
raise SystemExit(
|
|
246
238
|
"clip_plan QC blocking: fix unsafe sentence boundaries or target-duration drift. "
|
|
247
239
|
"Only duration drift can be explicitly accepted with --allow-duration-drift; "
|
|
@@ -305,12 +297,12 @@ def main():
|
|
|
305
297
|
)
|
|
306
298
|
for w in report["warnings"]:
|
|
307
299
|
log(f" ⚠️ 剪后解说同步: {w['message']} [{w['code']}]")
|
|
308
|
-
if report
|
|
300
|
+
if report["clamped_count"]:
|
|
309
301
|
raise SystemExit(
|
|
310
302
|
"剪后解说有句子被 clip 边界裁断。移动 clip_plan 边界或重写整句后重跑;"
|
|
311
303
|
"--allow-sparse-cut 不能跳过语句完整性门禁。详见 narration_mapped_lint.json"
|
|
312
304
|
)
|
|
313
|
-
if report
|
|
305
|
+
if report["blocking"] and not args.allow_sparse_cut:
|
|
314
306
|
raise SystemExit(
|
|
315
307
|
"剪后解说与保留片段对不上:丢弃过多或成片过稀疏。改 narration.json / clip_plan.json "
|
|
316
308
|
"让解说落在保留片段内后重跑,或加 --allow-sparse-cut 接受当前映射。详见 narration_mapped_lint.json"
|
|
@@ -114,14 +114,11 @@ def value_fingerprint(value):
|
|
|
114
114
|
|
|
115
115
|
def cut_plan_fingerprint(validated_plan):
|
|
116
116
|
"""Hash the exact normalized clip plan that determines edited_source.mp4 bytes."""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
payload.pop("qc", None)
|
|
123
|
-
else:
|
|
124
|
-
payload = validated_plan
|
|
117
|
+
payload = dict(validated_plan)
|
|
118
|
+
# Provenance for raw-plan freshness is not part of the edited media bytes.
|
|
119
|
+
payload.pop("raw_plan_fingerprint", None)
|
|
120
|
+
# QC is observability derived from the media plan, not an input range decision.
|
|
121
|
+
payload.pop("qc", None)
|
|
125
122
|
return value_fingerprint(payload)
|
|
126
123
|
|
|
127
124
|
|
|
@@ -169,26 +166,19 @@ def _load_edited_source_meta(output_path):
|
|
|
169
166
|
if not meta_path.exists():
|
|
170
167
|
return None
|
|
171
168
|
try:
|
|
172
|
-
|
|
169
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
173
170
|
except (OSError, ValueError):
|
|
174
171
|
return None
|
|
175
|
-
return
|
|
172
|
+
return meta if isinstance(meta, dict) else None
|
|
176
173
|
|
|
177
174
|
|
|
178
175
|
def _source_fingerprints_for_plan(validated_plan, input_video=None):
|
|
179
176
|
"""Fingerprint every media file that can affect edited_source.mp4 bytes."""
|
|
180
|
-
|
|
181
|
-
for clip in
|
|
182
|
-
validated_plan.get("clips", []) if isinstance(validated_plan, dict) else []
|
|
183
|
-
):
|
|
184
|
-
if clip.get("source_path"):
|
|
185
|
-
paths.append(str(clip["source_path"]))
|
|
177
|
+
# Single-source plans carry no per-clip source_path; the CLI video is the only input.
|
|
178
|
+
paths = {clip["source_path"] for clip in validated_plan["clips"] if "source_path" in clip}
|
|
186
179
|
if not paths and input_video is not None:
|
|
187
|
-
paths.
|
|
188
|
-
|
|
189
|
-
for path in sorted(set(paths)):
|
|
190
|
-
fingerprints[str(Path(path))] = file_fingerprint(path)
|
|
191
|
-
return fingerprints
|
|
180
|
+
paths.add(str(input_video))
|
|
181
|
+
return {str(Path(path)): file_fingerprint(path) for path in sorted(paths)}
|
|
192
182
|
|
|
193
183
|
|
|
194
184
|
def edited_source_render_cache_payload():
|
|
@@ -200,9 +190,7 @@ def edited_source_render_cache_payload():
|
|
|
200
190
|
return {
|
|
201
191
|
"render_algorithm_version": EDITED_SOURCE_RENDER_ALGORITHM_VERSION,
|
|
202
192
|
"geometry_render_algorithm_version": GEOMETRY_RENDER_ALGORITHM_VERSION,
|
|
203
|
-
"clip_join_audio_fade_ms": round(
|
|
204
|
-
max(0.0, float(CONFIG.get("clip_join_audio_fade_ms", 30.0) or 0.0)), 3
|
|
205
|
-
),
|
|
193
|
+
"clip_join_audio_fade_ms": round(CONFIG["clip_join_audio_fade_ms"], 3),
|
|
206
194
|
}
|
|
207
195
|
|
|
208
196
|
|
|
@@ -211,38 +199,17 @@ def edited_source_render_fingerprint():
|
|
|
211
199
|
|
|
212
200
|
|
|
213
201
|
def _write_edited_source_meta(output_path, validated_plan, input_video=None):
|
|
214
|
-
meta_path = _edited_source_meta_path(output_path)
|
|
215
|
-
source_fingerprints = _source_fingerprints_for_plan(validated_plan, input_video)
|
|
216
|
-
has_plan_sources = any(
|
|
217
|
-
clip.get("source_path")
|
|
218
|
-
for clip in (
|
|
219
|
-
validated_plan.get("clips", []) if isinstance(validated_plan, dict) else []
|
|
220
|
-
)
|
|
221
|
-
)
|
|
222
|
-
legacy_source_fp = (
|
|
223
|
-
file_fingerprint(input_video)
|
|
224
|
-
if input_video is not None
|
|
225
|
-
and not has_plan_sources
|
|
226
|
-
and len(source_fingerprints) == 1
|
|
227
|
-
else None
|
|
228
|
-
)
|
|
229
202
|
meta = {
|
|
230
203
|
"schema_version": 2,
|
|
231
204
|
"clip_plan_fingerprint": cut_plan_fingerprint(validated_plan),
|
|
232
205
|
"render_fingerprint": edited_source_render_fingerprint(),
|
|
233
206
|
"render_cache": edited_source_render_cache_payload(),
|
|
234
|
-
"source_fingerprints":
|
|
207
|
+
"source_fingerprints": _source_fingerprints_for_plan(validated_plan, input_video),
|
|
235
208
|
"edited_source_fingerprint": file_fingerprint(output_path),
|
|
236
|
-
"total_duration": validated_plan
|
|
237
|
-
"clip_count": len(validated_plan
|
|
209
|
+
"total_duration": validated_plan["total_duration"],
|
|
210
|
+
"clip_count": len(validated_plan["clips"]),
|
|
238
211
|
}
|
|
239
|
-
|
|
240
|
-
if delivery_qc:
|
|
241
|
-
meta["delivery_qc"] = delivery_qc
|
|
242
|
-
# Preserve the legacy key for existing single-source callers/tests/metadata readers.
|
|
243
|
-
if legacy_source_fp is not None:
|
|
244
|
-
meta["source_video_fingerprint"] = legacy_source_fp
|
|
245
|
-
meta_path.write_text(
|
|
212
|
+
_edited_source_meta_path(output_path).write_text(
|
|
246
213
|
json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
247
214
|
)
|
|
248
215
|
|
|
@@ -253,20 +220,20 @@ def should_reuse_edited_source(output_path, validated_plan, input_video=None):
|
|
|
253
220
|
if not output_path.exists():
|
|
254
221
|
return False
|
|
255
222
|
meta = _load_edited_source_meta(output_path)
|
|
256
|
-
if
|
|
257
|
-
|
|
223
|
+
if meta is None:
|
|
224
|
+
return False
|
|
225
|
+
if (
|
|
226
|
+
meta.get("clip_plan_fingerprint") != cut_plan_fingerprint(validated_plan)
|
|
227
|
+
or meta.get("render_fingerprint") != edited_source_render_fingerprint()
|
|
228
|
+
or meta.get("source_fingerprints")
|
|
229
|
+
!= _source_fingerprints_for_plan(validated_plan, input_video)
|
|
258
230
|
):
|
|
259
231
|
return False
|
|
260
|
-
|
|
232
|
+
try:
|
|
233
|
+
output_fingerprint = file_fingerprint(output_path)
|
|
234
|
+
except OSError:
|
|
261
235
|
return False
|
|
262
|
-
|
|
263
|
-
meta_sources = meta.get("source_fingerprints")
|
|
264
|
-
if meta_sources is None and input_video is not None:
|
|
265
|
-
meta_sources = {str(Path(input_video)): meta.get("source_video_fingerprint")}
|
|
266
|
-
return bool(
|
|
267
|
-
meta_sources == expected_sources
|
|
268
|
-
and meta.get("edited_source_fingerprint") == file_fingerprint(output_path)
|
|
269
|
-
)
|
|
236
|
+
return meta.get("edited_source_fingerprint") == output_fingerprint
|
|
270
237
|
|
|
271
238
|
|
|
272
239
|
def _manifest_source_entries(sources_manifest):
|
|
@@ -313,9 +280,11 @@ def normalize_sources_manifest(sources_manifest):
|
|
|
313
280
|
if duration in (None, ""):
|
|
314
281
|
duration = get_video_duration(source_path)
|
|
315
282
|
try:
|
|
316
|
-
duration =
|
|
283
|
+
duration = float(duration)
|
|
317
284
|
except (TypeError, ValueError) as exc:
|
|
318
285
|
raise ValueError(f"source {source_id} has invalid duration") from exc
|
|
286
|
+
if duration <= 0:
|
|
287
|
+
raise ValueError(f"source {source_id} has invalid duration")
|
|
319
288
|
sources[source_id] = {
|
|
320
289
|
"source_id": source_id,
|
|
321
290
|
"source_path": str(source_path),
|
|
@@ -358,8 +327,8 @@ def normalize_multi_source_clip_plan(
|
|
|
358
327
|
if not isinstance(raw_clips, list):
|
|
359
328
|
raise ValueError("clip_plan.json field `clips` must be an array")
|
|
360
329
|
|
|
361
|
-
padding = max(0.0,
|
|
362
|
-
min_duration = max(0.05,
|
|
330
|
+
padding = max(0.0, clip_padding)
|
|
331
|
+
min_duration = max(0.05, min_clip_duration)
|
|
363
332
|
clips = []
|
|
364
333
|
source_ranges = {}
|
|
365
334
|
cursor = 0.0
|
|
@@ -422,9 +391,7 @@ def normalize_multi_source_clip_plan(
|
|
|
422
391
|
plan = {
|
|
423
392
|
"clips": clips,
|
|
424
393
|
"total_duration": total_duration,
|
|
425
|
-
"target_duration": round(
|
|
426
|
-
if target_duration
|
|
427
|
-
else None,
|
|
394
|
+
"target_duration": round(target_duration, 3) if target_duration else None,
|
|
428
395
|
"sources": {
|
|
429
396
|
sid: {
|
|
430
397
|
"source_path": s["source_path"],
|
|
@@ -442,7 +409,7 @@ def normalize_multi_source_clip_plan(
|
|
|
442
409
|
if target_duration and total_duration > target_duration * 1.15:
|
|
443
410
|
plan["warning"] = (
|
|
444
411
|
f"validated clips total {total_duration:.1f}s exceeds target "
|
|
445
|
-
f"{
|
|
412
|
+
f"{target_duration:.1f}s by more than 15%"
|
|
446
413
|
)
|
|
447
414
|
log(f"警告: {plan['warning']}")
|
|
448
415
|
return plan
|
|
@@ -478,9 +445,8 @@ def normalize_clip_plan(
|
|
|
478
445
|
if not isinstance(raw_clips, list):
|
|
479
446
|
raise ValueError("clip_plan.json field `clips` must be an array")
|
|
480
447
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
min_duration = max(0.05, float(min_clip_duration or 0.05))
|
|
448
|
+
padding = max(0.0, clip_padding)
|
|
449
|
+
min_duration = max(0.05, min_clip_duration)
|
|
484
450
|
clips = []
|
|
485
451
|
source_ranges = []
|
|
486
452
|
cursor = 0.0
|
|
@@ -530,16 +496,14 @@ def normalize_clip_plan(
|
|
|
530
496
|
plan = {
|
|
531
497
|
"clips": clips,
|
|
532
498
|
"total_duration": total_duration,
|
|
533
|
-
"target_duration": round(
|
|
534
|
-
if target_duration
|
|
535
|
-
else None,
|
|
499
|
+
"target_duration": round(target_duration, 3) if target_duration else None,
|
|
536
500
|
"source_duration": round(video_duration, 3),
|
|
537
501
|
"allow_overlap": bool(allow_overlap),
|
|
538
502
|
}
|
|
539
503
|
if target_duration and total_duration > target_duration * 1.15:
|
|
540
504
|
plan["warning"] = (
|
|
541
505
|
f"validated clips total {total_duration:.1f}s exceeds target "
|
|
542
|
-
f"{
|
|
506
|
+
f"{target_duration:.1f}s by more than 15%"
|
|
543
507
|
)
|
|
544
508
|
log(f"警告: {plan['warning']}")
|
|
545
509
|
return plan
|
|
@@ -13,25 +13,11 @@ from sentence_boundaries import _continuous_source_join
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _audio_segment_filter(
|
|
16
|
-
label_in,
|
|
17
|
-
label_out,
|
|
18
|
-
start,
|
|
19
|
-
end,
|
|
20
|
-
duration,
|
|
21
|
-
fade_ms,
|
|
22
|
-
extra_filters="",
|
|
23
|
-
fade_in_ms=None,
|
|
24
|
-
fade_out_ms=None,
|
|
16
|
+
label_in, label_out, start, end, duration, fade_in_ms, fade_out_ms, extra_filters=""
|
|
25
17
|
):
|
|
26
|
-
max_fade =
|
|
27
|
-
fade_in = max(
|
|
28
|
-
|
|
29
|
-
min(float(fade_ms if fade_in_ms is None else fade_in_ms) / 1000.0, max_fade),
|
|
30
|
-
)
|
|
31
|
-
fade_out = max(
|
|
32
|
-
0.0,
|
|
33
|
-
min(float(fade_ms if fade_out_ms is None else fade_out_ms) / 1000.0, max_fade),
|
|
34
|
-
)
|
|
18
|
+
max_fade = duration / 2
|
|
19
|
+
fade_in = max(0.0, min(fade_in_ms / 1000.0, max_fade))
|
|
20
|
+
fade_out = max(0.0, min(fade_out_ms / 1000.0, max_fade))
|
|
35
21
|
base = f"{label_in}atrim=start={start:.3f}:end={end:.3f},asetpts=PTS-STARTPTS"
|
|
36
22
|
if fade_in > 0:
|
|
37
23
|
base += f",afade=t=in:st=0:d={fade_in:.3f}"
|
|
@@ -57,13 +43,12 @@ def _clip_audio_edge_fades(clips, idx, fade_ms):
|
|
|
57
43
|
return fade_in, fade_out
|
|
58
44
|
|
|
59
45
|
|
|
60
|
-
def _write_filter_script(filter_complex, work_dir):
|
|
61
|
-
script_path = Path(work_dir) / "edit_filter_complex.txt"
|
|
62
|
-
script_path.write_text(filter_complex, encoding="utf-8")
|
|
63
|
-
return script_path
|
|
64
|
-
|
|
65
|
-
|
|
66
46
|
def _probe_audio_sample_rate(video_path):
|
|
47
|
+
"""Sample rate of the first audio stream, or None when it cannot be observed.
|
|
48
|
+
|
|
49
|
+
Delivery QC is observational and runs even on a plan that was never rendered, so an
|
|
50
|
+
ffprobe that is absent (OSError) reads the same as one that reports no audio stream.
|
|
51
|
+
"""
|
|
67
52
|
cmd = [
|
|
68
53
|
"ffprobe",
|
|
69
54
|
"-v",
|
|
@@ -78,46 +63,33 @@ def _probe_audio_sample_rate(video_path):
|
|
|
78
63
|
]
|
|
79
64
|
try:
|
|
80
65
|
result = run_cmd(cmd)
|
|
81
|
-
except
|
|
66
|
+
except OSError:
|
|
82
67
|
return None
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
return int(float((result.stdout or "").strip().splitlines()[0]))
|
|
87
|
-
except (IndexError, TypeError, ValueError):
|
|
68
|
+
text = result.stdout.strip()
|
|
69
|
+
if result.returncode != 0 or not text:
|
|
88
70
|
return None
|
|
71
|
+
return int(float(text))
|
|
89
72
|
|
|
90
73
|
|
|
91
74
|
def _delivery_reencode_reason(source_paths, clips):
|
|
92
75
|
reasons = ["trim_concat_filter_requires_reencode"]
|
|
93
|
-
if len(source_paths
|
|
76
|
+
if len(source_paths) > 1:
|
|
94
77
|
reasons.append("multi_source_geometry_audio_normalization")
|
|
95
|
-
if any(
|
|
78
|
+
if any("source_path" not in clip for clip in clips):
|
|
96
79
|
reasons.append("single_source_filter_concat_no_stream_copy")
|
|
97
80
|
return "+".join(reasons)
|
|
98
81
|
|
|
99
82
|
|
|
100
|
-
def update_delivery_qc(
|
|
101
|
-
validated_plan, *, source_paths=None, output_path=None, rendered=False
|
|
102
|
-
):
|
|
83
|
+
def update_delivery_qc(validated_plan, *, source_paths, output_path=None, rendered=False):
|
|
103
84
|
"""Attach cut delivery facts to qc.delivery_qc without writing visual_qc."""
|
|
104
|
-
qc = validated_plan
|
|
105
|
-
clips = validated_plan
|
|
106
|
-
if source_paths is None:
|
|
107
|
-
source_paths = sorted(
|
|
108
|
-
{
|
|
109
|
-
str(clip.get("source_path") or "")
|
|
110
|
-
for clip in clips
|
|
111
|
-
if str(clip.get("source_path") or "")
|
|
112
|
-
}
|
|
113
|
-
)
|
|
85
|
+
qc = validated_plan["qc"]
|
|
86
|
+
clips = validated_plan["clips"]
|
|
114
87
|
target_sample_rate = 48000
|
|
115
88
|
probed_sample_rate = (
|
|
116
89
|
_probe_audio_sample_rate(output_path)
|
|
117
90
|
if output_path and Path(output_path).exists()
|
|
118
91
|
else None
|
|
119
92
|
)
|
|
120
|
-
output_geometry = qc.get("output_geometry")
|
|
121
93
|
delivery_qc = {
|
|
122
94
|
"schema_version": 1,
|
|
123
95
|
"video_encode_passes": 1,
|
|
@@ -135,9 +107,9 @@ def update_delivery_qc(
|
|
|
135
107
|
"audio encoded as AAC with 48000 Hz target for delivery compatibility",
|
|
136
108
|
"edited_source.mp4 is an intermediate; downstream assembly may perform another intentional encode",
|
|
137
109
|
],
|
|
138
|
-
"output_geometry": output_geometry,
|
|
139
|
-
"rendered":
|
|
140
|
-
"planned": not
|
|
110
|
+
"output_geometry": qc["output_geometry"],
|
|
111
|
+
"rendered": rendered,
|
|
112
|
+
"planned": not rendered,
|
|
141
113
|
}
|
|
142
114
|
if probed_sample_rate and probed_sample_rate != target_sample_rate:
|
|
143
115
|
delivery_qc["final_compat_notes"].append(
|
|
@@ -148,12 +120,10 @@ def update_delivery_qc(
|
|
|
148
120
|
|
|
149
121
|
|
|
150
122
|
def write_cut_delivery_qc(work_dir, validated_plan):
|
|
151
|
-
delivery_qc = (validated_plan.get("qc") or {}).get("delivery_qc")
|
|
152
|
-
if not delivery_qc or not delivery_qc.get("rendered"):
|
|
153
|
-
return None
|
|
154
123
|
path = Path(work_dir) / "cut_delivery_qc.json"
|
|
155
124
|
path.write_text(
|
|
156
|
-
json.dumps(delivery_qc, ensure_ascii=False, indent=2),
|
|
125
|
+
json.dumps(validated_plan["qc"]["delivery_qc"], ensure_ascii=False, indent=2),
|
|
126
|
+
encoding="utf-8",
|
|
157
127
|
)
|
|
158
128
|
return path
|
|
159
129
|
|
|
@@ -163,23 +133,21 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
163
133
|
work_dir = Path(work_dir)
|
|
164
134
|
output_path = Path(output_path or work_dir / "edited_source.mp4")
|
|
165
135
|
clips = validated_plan["clips"]
|
|
166
|
-
if not clips:
|
|
167
|
-
raise ValueError("validated clip plan has no clips")
|
|
168
136
|
|
|
169
137
|
source_paths = []
|
|
170
138
|
for clip in clips:
|
|
171
|
-
source_path =
|
|
139
|
+
source_path = clip.get("source_path", str(input_video))
|
|
172
140
|
if source_path not in source_paths:
|
|
173
141
|
source_paths.append(source_path)
|
|
174
142
|
source_index = {path: idx for idx, path in enumerate(source_paths)}
|
|
175
143
|
audio_by_input = {path: _has_audio_stream(path) for path in source_paths}
|
|
176
|
-
join_fade_ms =
|
|
144
|
+
join_fade_ms = CONFIG["clip_join_audio_fade_ms"]
|
|
177
145
|
qc = validated_plan.setdefault("qc", {})
|
|
178
146
|
qc["join_fade_ms"] = round(join_fade_ms, 3)
|
|
179
|
-
if not qc
|
|
147
|
+
if "output_geometry" not in qc:
|
|
180
148
|
_, _, _, geometry_qc = _select_output_geometry(source_paths, clips)
|
|
181
149
|
qc["output_geometry"] = geometry_qc
|
|
182
|
-
qc["output_geometry_reason"] = geometry_qc
|
|
150
|
+
qc["output_geometry_reason"] = geometry_qc["reason"]
|
|
183
151
|
|
|
184
152
|
parts = []
|
|
185
153
|
concat_inputs = []
|
|
@@ -190,23 +158,12 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
190
158
|
# segment to one canvas and give every clip an audio segment (real or synthesized
|
|
191
159
|
# silence) so concat always succeeds with a continuous track and no source's audio
|
|
192
160
|
# is dropped just because a sibling source is silent.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
geometry_qc
|
|
198
|
-
)
|
|
199
|
-
canvas_w, canvas_h, canvas_fps = (
|
|
200
|
-
int(geometry_qc["width"]),
|
|
201
|
-
int(geometry_qc["height"]),
|
|
202
|
-
geometry_qc["fps"],
|
|
203
|
-
)
|
|
204
|
-
else:
|
|
205
|
-
canvas_w, canvas_h, canvas_fps, geometry_qc = _select_output_geometry(
|
|
206
|
-
source_paths, clips
|
|
207
|
-
)
|
|
208
|
-
qc["output_geometry"] = geometry_qc
|
|
209
|
-
qc["output_geometry_reason"] = geometry_qc.get("reason")
|
|
161
|
+
geometry_qc = qc["output_geometry"]
|
|
162
|
+
canvas_w, canvas_h, canvas_fps = (
|
|
163
|
+
geometry_qc["width"],
|
|
164
|
+
geometry_qc["height"],
|
|
165
|
+
geometry_qc["fps"],
|
|
166
|
+
)
|
|
210
167
|
vnorm = (
|
|
211
168
|
f"scale={canvas_w}:{canvas_h}:force_original_aspect_ratio=decrease,"
|
|
212
169
|
f"pad={canvas_w}:{canvas_h}:(ow-iw)/2:(oh-ih)/2,setsar=1,"
|
|
@@ -214,18 +171,16 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
214
171
|
)
|
|
215
172
|
for clip_pos, clip in enumerate(clips):
|
|
216
173
|
idx = clip["clip_id"]
|
|
217
|
-
clip_source =
|
|
174
|
+
clip_source = clip.get("source_path", str(input_video))
|
|
218
175
|
input_idx = source_index[clip_source]
|
|
219
176
|
start = clip["source_start"]
|
|
220
177
|
end = clip["source_end"]
|
|
221
|
-
dur =
|
|
222
|
-
fade_in_ms, fade_out_ms = _clip_audio_edge_fades(
|
|
223
|
-
clips, clip_pos, join_fade_ms
|
|
224
|
-
)
|
|
178
|
+
dur = end - start
|
|
179
|
+
fade_in_ms, fade_out_ms = _clip_audio_edge_fades(clips, clip_pos, join_fade_ms)
|
|
225
180
|
parts.append(
|
|
226
181
|
f"[{input_idx}:v]trim=start={start:.3f}:end={end:.3f},setpts=PTS-STARTPTS,{vnorm}[v{idx}]"
|
|
227
182
|
)
|
|
228
|
-
if audio_by_input
|
|
183
|
+
if audio_by_input[clip_source]:
|
|
229
184
|
parts.append(
|
|
230
185
|
_audio_segment_filter(
|
|
231
186
|
f"[{input_idx}:a]",
|
|
@@ -233,10 +188,9 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
233
188
|
start,
|
|
234
189
|
end,
|
|
235
190
|
dur,
|
|
236
|
-
|
|
191
|
+
fade_in_ms,
|
|
192
|
+
fade_out_ms,
|
|
237
193
|
extra_filters="aresample=48000,aformat=sample_rates=48000:channel_layouts=stereo",
|
|
238
|
-
fade_in_ms=fade_in_ms,
|
|
239
|
-
fade_out_ms=fade_out_ms,
|
|
240
194
|
)
|
|
241
195
|
)
|
|
242
196
|
else:
|
|
@@ -251,7 +205,7 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
251
205
|
has_audio = all(audio_by_input.values())
|
|
252
206
|
for clip_pos, clip in enumerate(clips):
|
|
253
207
|
idx = clip["clip_id"]
|
|
254
|
-
input_idx = source_index[
|
|
208
|
+
input_idx = source_index[clip.get("source_path", str(input_video))]
|
|
255
209
|
start = clip["source_start"]
|
|
256
210
|
end = clip["source_end"]
|
|
257
211
|
parts.append(
|
|
@@ -268,10 +222,9 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
268
222
|
f"[a{idx}]",
|
|
269
223
|
start,
|
|
270
224
|
end,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
fade_out_ms=fade_out_ms,
|
|
225
|
+
end - start,
|
|
226
|
+
fade_in_ms,
|
|
227
|
+
fade_out_ms,
|
|
275
228
|
)
|
|
276
229
|
)
|
|
277
230
|
concat_inputs.append(f"[a{idx}]")
|
|
@@ -282,23 +235,21 @@ def build_edited_source_video(input_video, validated_plan, work_dir, output_path
|
|
|
282
235
|
)
|
|
283
236
|
maps = ["-map", "[v]", "-map", "[a]"]
|
|
284
237
|
else:
|
|
285
|
-
total = validated_plan.get("total_duration") or sum(
|
|
286
|
-
c["duration"] for c in clips
|
|
287
|
-
)
|
|
288
238
|
parts.append("".join(concat_inputs) + f"concat=n={len(clips)}:v=1:a=0[v]")
|
|
289
239
|
maps = ["-map", "[v]", "-map", f"{len(source_paths)}:a", "-shortest"]
|
|
290
240
|
extra_inputs = [
|
|
291
241
|
"-f",
|
|
292
242
|
"lavfi",
|
|
293
243
|
"-t",
|
|
294
|
-
f"{
|
|
244
|
+
f"{validated_plan['total_duration']:.3f}",
|
|
295
245
|
"-i",
|
|
296
246
|
"anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
297
247
|
]
|
|
298
248
|
|
|
299
249
|
filter_complex = ";".join(parts)
|
|
300
250
|
if len(filter_complex.encode("utf-8")) > 7000:
|
|
301
|
-
filter_script =
|
|
251
|
+
filter_script = work_dir / "edit_filter_complex.txt"
|
|
252
|
+
filter_script.write_text(filter_complex, encoding="utf-8")
|
|
302
253
|
filter_args = ["-filter_complex_script", str(filter_script)]
|
|
303
254
|
else:
|
|
304
255
|
filter_args = ["-filter_complex", filter_complex]
|