@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
|
@@ -59,7 +59,7 @@ def _explicit_source_video():
|
|
|
59
59
|
"""Return the cut-mode source video only when the caller opted in explicitly."""
|
|
60
60
|
if not CONFIG.get("source_video_explicit", False):
|
|
61
61
|
return ""
|
|
62
|
-
return
|
|
62
|
+
return CONFIG["source_video"]
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _source_video_identity():
|
|
@@ -72,17 +72,12 @@ def _source_video_identity():
|
|
|
72
72
|
|
|
73
73
|
def _timeline_provenance_status(work_dir):
|
|
74
74
|
data = _load_work_json(work_dir, "timeline.json")
|
|
75
|
-
if
|
|
76
|
-
return None
|
|
77
|
-
provenance = data.get("provenance")
|
|
78
|
-
return provenance if isinstance(provenance, dict) else None
|
|
75
|
+
return None if data is None else data["provenance"]
|
|
79
76
|
|
|
80
77
|
|
|
81
78
|
def _load_work_json(work_dir, name):
|
|
79
|
+
"""Parse a JSON artifact in work_dir; None only when the file does not exist."""
|
|
82
80
|
path = Path(work_dir) / name
|
|
83
81
|
if not path.exists():
|
|
84
82
|
return None
|
|
85
|
-
|
|
86
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
87
|
-
except (ValueError, OSError):
|
|
88
|
-
return None
|
|
83
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -34,6 +34,7 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
34
34
|
|
|
35
35
|
video_duration = lib.get_video_duration(input_video)
|
|
36
36
|
canvas = media._probe_canvas(input_video) # drives subtitle PlayRes/scale so 竖屏 text isn't stretched
|
|
37
|
+
burn_subtitles = lib.CONFIG["burn_subtitles"]
|
|
37
38
|
|
|
38
39
|
# 解说整体提速(可选)后,将所有 TTS 片段按时间位置合成到与视频等长的音轨上
|
|
39
40
|
narration_audio._apply_narration_speed(tts_segments, work_dir)
|
|
@@ -53,25 +54,23 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
53
54
|
srt_path = subtitle_render._generate_srt(tts_segments, work_dir, video_duration)
|
|
54
55
|
lib.log(f"字幕文件: {srt_path}")
|
|
55
56
|
ass_path = None
|
|
56
|
-
if
|
|
57
|
+
if burn_subtitles:
|
|
57
58
|
ass_path = subtitle_render._generate_ass(tts_segments, work_dir, video_duration, canvas)
|
|
58
59
|
lib.log(f"压制字幕文件: {ass_path}")
|
|
59
60
|
|
|
60
61
|
# 可选 BGM:作为一条独立音轨(input [2:a])混入,旁白处自动压低
|
|
61
|
-
bgm_path = lib.CONFIG
|
|
62
|
+
bgm_path = lib.CONFIG["bgm_path"]
|
|
62
63
|
has_bgm = bool(bgm_path) and os.path.exists(bgm_path)
|
|
63
64
|
if bgm_path and not has_bgm:
|
|
64
65
|
lib.log(f" ⚠️ BGM 文件不存在,跳过: {bgm_path}")
|
|
65
66
|
elif has_bgm:
|
|
66
|
-
lib.log(f"BGM 铺底: {bgm_path} (音量 {lib.CONFIG
|
|
67
|
+
lib.log(f"BGM 铺底: {bgm_path} (音量 {lib.CONFIG['bgm_volume']},旁白时 {lib.CONFIG['bgm_ducking_volume']})")
|
|
67
68
|
|
|
68
69
|
# 多轨时间线模型(timeline.json):canonical 渲染仍是 ffmpeg,此模型供检视/可选导出
|
|
69
|
-
timeline_emit._emit_timeline(input_video, tts_segments, work_dir, video_duration, has_bgm)
|
|
70
|
+
timeline_emit._emit_timeline(input_video, tts_segments, work_dir, video_duration, canvas, has_bgm)
|
|
70
71
|
|
|
71
72
|
overlay_filters, overlay_qc = visual_render._visual_overlay_filters(work_dir, canvas, video_duration)
|
|
72
|
-
mask_filter = visual_render._source_subtitle_mask_filter(
|
|
73
|
-
canvas, work_dir, tts_segments, video_duration=video_duration
|
|
74
|
-
)
|
|
73
|
+
mask_filter = visual_render._source_subtitle_mask_filter(canvas, work_dir, tts_segments, video_duration)
|
|
75
74
|
visual_qc = visual_render._build_visual_qc(
|
|
76
75
|
tts_segments,
|
|
77
76
|
work_dir,
|
|
@@ -83,8 +82,8 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
83
82
|
visual_render._write_visual_qc(work_dir, visual_qc)
|
|
84
83
|
assembly_qc_path = Path(work_dir) / constants.ASSEMBLY_QC
|
|
85
84
|
assembly_qc_path.unlink(missing_ok=True)
|
|
86
|
-
if visual_qc
|
|
87
|
-
codes = ", ".join(visual_qc
|
|
85
|
+
if visual_qc["blocking"]:
|
|
86
|
+
codes = ", ".join(visual_qc["blocking_codes"])
|
|
88
87
|
raise RuntimeError(f"视觉 QC 失败: {codes};详见 {Path(work_dir) / constants.VISUAL_QC}")
|
|
89
88
|
|
|
90
89
|
# 混合原始音频 + 解说音频(+ 可选 BGM)
|
|
@@ -112,9 +111,7 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
112
111
|
# duration=first + -t trim it back to the video length).
|
|
113
112
|
bgm_input = ["-stream_loop", "-1", "-i", str(bgm_path)] if has_bgm else []
|
|
114
113
|
|
|
115
|
-
# 对于超长 volume 表达式(多段解说),使用 -filter_complex_script 避免命令行溢出
|
|
116
114
|
# 末端整体响度归一:ducking 只管相对平衡,这一步统一成片绝对响度
|
|
117
|
-
aout_label = "[aout]"
|
|
118
115
|
loudnorm_measurement = audio_mix._run_loudnorm_first_pass(
|
|
119
116
|
input_video,
|
|
120
117
|
narration_wav,
|
|
@@ -124,54 +121,42 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
124
121
|
work_dir,
|
|
125
122
|
)
|
|
126
123
|
final_ln = audio_mix.final_loudnorm_filter(loudnorm_measurement)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
aout_label = "[aoutln]"
|
|
130
|
-
lib.log(f"成片响度归一: {final_ln}")
|
|
124
|
+
filter_complex += f";[aout]{final_ln}[aoutln]"
|
|
125
|
+
lib.log(f"成片响度归一: {final_ln}")
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
# 对于超长 volume 表达式(多段解说),使用 -filter_complex_script 避免命令行溢出
|
|
128
|
+
fc_script = None
|
|
129
|
+
if len(filter_complex.encode("utf-8")) > constants.FILTER_SCRIPT_THRESHOLD_BYTES:
|
|
134
130
|
fc_script = Path(work_dir) / ".filter_complex.txt"
|
|
135
131
|
fc_script.write_text(filter_complex, encoding="utf-8")
|
|
136
|
-
lib.log(f"使用 filter_complex_script (表达式长度 {len(
|
|
137
|
-
|
|
138
|
-
"ffmpeg", "-y",
|
|
139
|
-
"-i", str(input_video),
|
|
140
|
-
"-i", str(narration_wav),
|
|
141
|
-
*original_audio_input,
|
|
142
|
-
*bgm_input,
|
|
143
|
-
"-filter_complex_script", str(fc_script),
|
|
144
|
-
# 0:v:0 (not 0:v): sources with attached cover art carry a second video stream.
|
|
145
|
-
# -vf only ever applies to the first one, so mapping all of them makes ffmpeg
|
|
146
|
-
# abort with "Could not write header (incorrect codec parameters ?)" and leave
|
|
147
|
-
# an unreadable file — after the whole pipeline has already run.
|
|
148
|
-
"-map", "0:v:0", "-map", aout_label,
|
|
149
|
-
]
|
|
132
|
+
lib.log(f"使用 filter_complex_script (表达式长度 {len(filter_complex.encode('utf-8'))} bytes)")
|
|
133
|
+
filter_args = ["-filter_complex_script", str(fc_script)]
|
|
150
134
|
else:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
]
|
|
135
|
+
filter_args = ["-filter_complex", filter_complex]
|
|
136
|
+
cmd = [
|
|
137
|
+
"ffmpeg", "-y",
|
|
138
|
+
"-i", str(input_video),
|
|
139
|
+
"-i", str(narration_wav),
|
|
140
|
+
*original_audio_input,
|
|
141
|
+
*bgm_input,
|
|
142
|
+
*filter_args,
|
|
143
|
+
# 0:v:0 (not 0:v): sources with attached cover art carry a second video stream.
|
|
144
|
+
# -vf only ever applies to the first one, so mapping all of them makes ffmpeg
|
|
145
|
+
# abort with "Could not write header (incorrect codec parameters ?)" and leave
|
|
146
|
+
# an unreadable file — after the whole pipeline has already run.
|
|
147
|
+
"-map", "0:v:0", "-map", "[aoutln]",
|
|
148
|
+
]
|
|
164
149
|
|
|
165
150
|
# Video filter chain: mask source subtitles first (drawbox), then burn our subtitles
|
|
166
151
|
# on top. Either one forces a re-encode; with neither, the video stream is copied.
|
|
167
|
-
crf = str(lib.CONFIG
|
|
168
|
-
preset =
|
|
169
|
-
max_h =
|
|
152
|
+
crf = str(lib.CONFIG["output_crf"]) # env_int already clamps to >=0; keep 0 (lossless) intact
|
|
153
|
+
preset = lib.CONFIG["output_preset"]
|
|
154
|
+
max_h = lib.CONFIG["output_max_height"]
|
|
170
155
|
vf_chain = []
|
|
171
156
|
if mask_filter:
|
|
172
157
|
vf_chain.append(mask_filter)
|
|
173
158
|
vf_chain.extend(overlay_filters)
|
|
174
|
-
if
|
|
159
|
+
if burn_subtitles:
|
|
175
160
|
vf_chain.append(visual_render._subtitle_burn_filter(ass_path))
|
|
176
161
|
# Downscale LAST so the mask + burned subtitles render at native resolution and are then
|
|
177
162
|
# scaled down with the frame (crisp). The helper forces both dimensions even so an odd
|
|
@@ -183,6 +168,7 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
183
168
|
# needs EVEN width AND height, so normalize odd dims (4:2:2/4:4:4 permit them) before the
|
|
184
169
|
# encode — otherwise libx264 aborts to a 0-byte file. The downscale helper already evens out.
|
|
185
170
|
even = "scale=trunc(iw/2)*2:trunc(ih/2)*2"
|
|
171
|
+
reencode = bool(vf_chain) or lib.CONFIG["force_video_reencode"]
|
|
186
172
|
notes = []
|
|
187
173
|
video_filter_script = None
|
|
188
174
|
if vf_chain:
|
|
@@ -202,10 +188,10 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
202
188
|
cmd += ["-c:v", "libx264", "-preset", preset, "-crf", crf, "-pix_fmt", "yuv420p"]
|
|
203
189
|
notes = ((["遮挡原字幕"] if mask_filter else [])
|
|
204
190
|
+ ([f"视觉叠加×{len(overlay_filters)}"] if overlay_filters else [])
|
|
205
|
-
+ (["压制解说字幕"] if
|
|
191
|
+
+ (["压制解说字幕"] if burn_subtitles else [])
|
|
206
192
|
+ ([f"缩放≤{max_h}p"] if max_h > 0 else []))
|
|
207
193
|
lib.log(f"视频重编码: {' + '.join(notes)} (crf={crf}, preset={preset})")
|
|
208
|
-
elif
|
|
194
|
+
elif reencode:
|
|
209
195
|
notes = ["force_video_reencode"]
|
|
210
196
|
cmd += ["-vf", even, "-c:v", "libx264", "-preset", preset, "-crf", crf, "-pix_fmt", "yuv420p"]
|
|
211
197
|
else:
|
|
@@ -220,8 +206,8 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
220
206
|
if result.returncode != 0:
|
|
221
207
|
raise RuntimeError(f"视频组装失败: {result.stderr}")
|
|
222
208
|
finally:
|
|
223
|
-
# 清理临时
|
|
224
|
-
if
|
|
209
|
+
# 清理临时 filter 脚本(无论 ffmpeg 是否成功)
|
|
210
|
+
if fc_script is not None:
|
|
225
211
|
fc_script.unlink(missing_ok=True)
|
|
226
212
|
if video_filter_script is not None:
|
|
227
213
|
video_filter_script.unlink(missing_ok=True)
|
|
@@ -237,10 +223,10 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
237
223
|
loudnorm_measurement=loudnorm_measurement,
|
|
238
224
|
visual_qc=visual_qc,
|
|
239
225
|
render_delivery={
|
|
240
|
-
"video_encode_passes": 1 if
|
|
241
|
-
"reencode_reason": notes
|
|
226
|
+
"video_encode_passes": 1 if reencode else 0,
|
|
227
|
+
"reencode_reason": notes,
|
|
242
228
|
"audio_sample_rate": 48000,
|
|
243
|
-
"final_compat_notes": ["yuv420p"] if
|
|
229
|
+
"final_compat_notes": ["yuv420p"] if reencode else ["video_copy", "aac_48000", "faststart"],
|
|
244
230
|
},
|
|
245
231
|
),
|
|
246
232
|
)
|
|
@@ -250,7 +236,6 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
250
236
|
def main():
|
|
251
237
|
import argparse
|
|
252
238
|
import shutil
|
|
253
|
-
from pathlib import Path
|
|
254
239
|
ap = argparse.ArgumentParser(
|
|
255
240
|
description="video-assemble: mux narration audio over the video, duck the original, render subtitles.")
|
|
256
241
|
ap.add_argument("video", help="source video (edited_source.mp4 in cut mode, else the original)")
|
|
@@ -294,6 +279,8 @@ def main():
|
|
|
294
279
|
if "SUBTITLE_MASK_OPACITY" not in os.environ:
|
|
295
280
|
lib.CONFIG["subtitle_mask_opacity"] = 1.0
|
|
296
281
|
if args.source_video:
|
|
282
|
+
if not os.path.exists(args.source_video):
|
|
283
|
+
ap.error(f"--source-video does not exist: {args.source_video}")
|
|
297
284
|
lib.CONFIG["source_video"] = args.source_video
|
|
298
285
|
lib.CONFIG["source_video_explicit"] = True
|
|
299
286
|
else:
|
|
@@ -313,9 +300,9 @@ def main():
|
|
|
313
300
|
tts_segments = json.loads(tts_meta.read_text(encoding="utf-8"))["segments"]
|
|
314
301
|
output_path = work_dir / "output.mp4"
|
|
315
302
|
assemble_video(args.video, tts_segments, work_dir, output_path)
|
|
316
|
-
assembly_qc = artifacts._load_work_json(work_dir, constants.ASSEMBLY_QC)
|
|
317
|
-
if assembly_qc
|
|
318
|
-
codes = ", ".join(assembly_qc
|
|
303
|
+
assembly_qc = artifacts._load_work_json(work_dir, constants.ASSEMBLY_QC)
|
|
304
|
+
if assembly_qc["blocking"]:
|
|
305
|
+
codes = ", ".join(assembly_qc["blocking_codes"])
|
|
319
306
|
raise SystemExit(f"组装 QC 阻断交付: {codes};详见 {work_dir / constants.ASSEMBLY_QC}")
|
|
320
307
|
stem = args.recap_stem or Path(args.video).stem
|
|
321
308
|
base = Path(args.output_dir) if args.output_dir else work_dir.parent
|
|
@@ -333,7 +320,7 @@ def main():
|
|
|
333
320
|
|
|
334
321
|
# OPTIONAL, decoupled: export a 剪映 draft from the timeline (lazy import; never
|
|
335
322
|
# required by the core render path).
|
|
336
|
-
if lib.CONFIG
|
|
323
|
+
if lib.CONFIG["export_jianying"]:
|
|
337
324
|
from jianying_optional import _maybe_export_jianying
|
|
338
325
|
_maybe_export_jianying(work_dir, args.jianying_out, stem)
|
|
339
326
|
|
|
@@ -10,7 +10,6 @@ from assemble_constants import (
|
|
|
10
10
|
ASSEMBLY_MANIFEST,
|
|
11
11
|
ASSEMBLY_QC,
|
|
12
12
|
SEGMENT_AUDIO_SCHEMA_VERSION,
|
|
13
|
-
VISUAL_QC,
|
|
14
13
|
)
|
|
15
14
|
from audio_mix import _loudness_mode
|
|
16
15
|
from artifacts import (
|
|
@@ -19,6 +18,12 @@ from artifacts import (
|
|
|
19
18
|
_timeline_provenance_status,
|
|
20
19
|
)
|
|
21
20
|
|
|
21
|
+
_AUDIO_QC_CODES = frozenset({
|
|
22
|
+
"missing_narration", "skipped_segments", "no_safe_fit", "effective_tempo_exceeded",
|
|
23
|
+
"empty_narration", "truncated_speech", "unsafe_source_handoff", "timeline_audio_mismatch",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
|
|
22
27
|
def _assembly_manifest_payload(input_video, tts_segments, work_dir, output_path,
|
|
23
28
|
tts_meta_path=None, final_output=None, *, settings_fingerprint):
|
|
24
29
|
"""Slim render record. The orchestrator reads `final_output` to report the result;
|
|
@@ -28,34 +33,34 @@ def _assembly_manifest_payload(input_video, tts_segments, work_dir, output_path,
|
|
|
28
33
|
output_path = Path(output_path)
|
|
29
34
|
source_video, source_video_fingerprint = _source_video_identity()
|
|
30
35
|
qc_path = Path(work_dir) / ASSEMBLY_QC
|
|
31
|
-
qc = _load_work_json(work_dir, ASSEMBLY_QC)
|
|
36
|
+
qc = _load_work_json(work_dir, ASSEMBLY_QC) or {}
|
|
32
37
|
payload = {
|
|
33
38
|
"schema_version": 2,
|
|
34
39
|
"input_video": str(input_video.resolve()),
|
|
35
40
|
"source_video": source_video,
|
|
36
41
|
"source_video_fingerprint": source_video_fingerprint,
|
|
37
42
|
"tts_meta": str(Path(tts_meta_path).resolve()) if tts_meta_path else None,
|
|
38
|
-
"tts_segments": len(tts_segments
|
|
43
|
+
"tts_segments": len(tts_segments),
|
|
39
44
|
"assembly_settings": settings_fingerprint(work_dir),
|
|
40
45
|
"output_path": str(output_path.resolve()),
|
|
41
46
|
"segment_audio_schema_version": SEGMENT_AUDIO_SCHEMA_VERSION,
|
|
42
|
-
"qc_path": str(qc_path.resolve()) if
|
|
43
|
-
"qc_verdict": qc.get("verdict")
|
|
44
|
-
"qc_blocking_codes": qc.get("blocking_codes", [])
|
|
47
|
+
"qc_path": str(qc_path.resolve()) if qc else None,
|
|
48
|
+
"qc_verdict": qc.get("verdict"),
|
|
49
|
+
"qc_blocking_codes": qc.get("blocking_codes", []),
|
|
45
50
|
# The settings fingerprint records the configured/fallback loudness policy; these QC
|
|
46
51
|
# fields record what the just-finished render actually used after the loudnorm probe.
|
|
47
|
-
"qc_loudness_mode": qc.get("loudness_mode")
|
|
48
|
-
"qc_loudnorm_measurement": qc.get("loudnorm_measurement")
|
|
52
|
+
"qc_loudness_mode": qc.get("loudness_mode"),
|
|
53
|
+
"qc_loudnorm_measurement": qc.get("loudnorm_measurement"),
|
|
49
54
|
"audio_segments": [
|
|
50
55
|
{
|
|
51
|
-
"index": seg
|
|
56
|
+
"index": seg["index"],
|
|
52
57
|
"segment_audio_schema_version": seg.get("segment_audio_schema_version", SEGMENT_AUDIO_SCHEMA_VERSION),
|
|
53
|
-
"narration": seg
|
|
54
|
-
"spoken_text": seg.get("spoken_text"
|
|
55
|
-
"truncated":
|
|
58
|
+
"narration": seg["narration"],
|
|
59
|
+
"spoken_text": seg.get("spoken_text", seg["narration"]),
|
|
60
|
+
"truncated": seg.get("truncated", False),
|
|
56
61
|
"truncate_reason": seg.get("truncate_reason", "none"),
|
|
57
62
|
"fit_status": seg.get("fit_status"),
|
|
58
|
-
"blocking":
|
|
63
|
+
"blocking": seg.get("blocking", False),
|
|
59
64
|
"audio_duration": seg.get("audio_duration"),
|
|
60
65
|
"placed_audio_duration": seg.get("placed_audio_duration"),
|
|
61
66
|
"placed_audio_path": seg.get("placed_audio_path"),
|
|
@@ -72,8 +77,7 @@ def _assembly_manifest_payload(input_video, tts_segments, work_dir, output_path,
|
|
|
72
77
|
"rms_dbfs_after": seg.get("rms_dbfs_after"),
|
|
73
78
|
"peak_after": seg.get("peak_after"),
|
|
74
79
|
}
|
|
75
|
-
for seg in
|
|
76
|
-
if isinstance(seg, dict)
|
|
80
|
+
for seg in tts_segments
|
|
77
81
|
],
|
|
78
82
|
}
|
|
79
83
|
if final_output is not None:
|
|
@@ -91,119 +95,89 @@ def _write_assembly_manifest(work_dir, manifest):
|
|
|
91
95
|
|
|
92
96
|
|
|
93
97
|
def _visual_qc_rollup(visual_qc):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"present": False,
|
|
97
|
-
"verdict": "MISSING",
|
|
98
|
-
"blocking": True,
|
|
99
|
-
"blocking_codes": ["missing_visual_qc"],
|
|
100
|
-
"summary": {},
|
|
101
|
-
}
|
|
102
|
-
subtitles = visual_qc.get("subtitles") or visual_qc.get("subtitle") or {}
|
|
103
|
-
overlays = visual_qc.get("overlays") or visual_qc.get("overlay") or {}
|
|
98
|
+
subtitles = visual_qc["subtitles"]
|
|
99
|
+
overlays = visual_qc["overlays"]
|
|
104
100
|
return {
|
|
105
101
|
"present": True,
|
|
106
|
-
"artifact": visual_qc
|
|
107
|
-
"verdict": visual_qc
|
|
108
|
-
"blocking":
|
|
109
|
-
"blocking_codes": list(visual_qc
|
|
110
|
-
"summary": visual_qc
|
|
111
|
-
"geometry": visual_qc
|
|
102
|
+
"artifact": visual_qc["artifact"],
|
|
103
|
+
"verdict": visual_qc["verdict"],
|
|
104
|
+
"blocking": visual_qc["blocking"],
|
|
105
|
+
"blocking_codes": list(visual_qc["blocking_codes"]),
|
|
106
|
+
"summary": visual_qc["summary"],
|
|
107
|
+
"geometry": visual_qc["geometry"],
|
|
112
108
|
"subtitles": {
|
|
113
|
-
"entries": subtitles
|
|
114
|
-
"multi_line": subtitles
|
|
115
|
-
"overflow": subtitles
|
|
116
|
-
"safe_area": subtitles
|
|
109
|
+
"entries": subtitles["entries"],
|
|
110
|
+
"multi_line": subtitles["multi_line"],
|
|
111
|
+
"overflow": subtitles["overflow"],
|
|
112
|
+
"safe_area": subtitles["safe_area"],
|
|
117
113
|
},
|
|
118
|
-
"
|
|
119
|
-
"mask": visual_qc.get("mask", {}),
|
|
114
|
+
"mask": visual_qc["mask"],
|
|
120
115
|
"overlays": {
|
|
121
|
-
"present": overlays
|
|
122
|
-
"rendered": overlays
|
|
116
|
+
"present": overlays["present"],
|
|
117
|
+
"rendered": overlays["rendered"],
|
|
123
118
|
"unsupported": overlays.get("unsupported", []),
|
|
124
119
|
"overflow": overlays.get("overflow", []),
|
|
125
120
|
},
|
|
126
|
-
"overlay": overlays,
|
|
127
121
|
}
|
|
128
122
|
|
|
129
123
|
|
|
124
|
+
def _placed_audio_matches_timeline(seg):
|
|
125
|
+
"""True when the persisted per-beat WAV is exactly what the serialized timeline window plays."""
|
|
126
|
+
placed_path = Path(seg["placed_audio_path"])
|
|
127
|
+
if not placed_path.exists():
|
|
128
|
+
return False
|
|
129
|
+
with wave.open(str(placed_path), "rb") as placed_wav:
|
|
130
|
+
placed_duration = placed_wav.getnframes() / placed_wav.getframerate()
|
|
131
|
+
tolerance = 1.0 / placed_wav.getframerate()
|
|
132
|
+
timeline_start = round(float(seg["actual_place_start"]), 4)
|
|
133
|
+
timeline_end = math.ceil(float(seg["actual_place_end"]) * 10_000 - 1e-9) / 10_000
|
|
134
|
+
serialized_span = timeline_end - timeline_start
|
|
135
|
+
return (
|
|
136
|
+
abs(placed_duration - seg["placed_audio_duration"]) <= tolerance + 1e-9
|
|
137
|
+
and serialized_span + 1e-9 >= placed_duration
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
130
141
|
def _build_assembly_qc(tts_segments, video_duration, *, output_path=None,
|
|
131
142
|
source_has_audio=None, loudness_mode=None, loudnorm_measurement=None,
|
|
132
|
-
visual_qc=None, render_delivery=None
|
|
143
|
+
visual_qc=None, render_delivery=None):
|
|
133
144
|
"""Machine-readable assembly release gate.
|
|
134
145
|
|
|
135
146
|
Visual facts are rolled up from visual_qc.json. Delivery/render facts live here
|
|
136
147
|
(or render/delivery QC in future), never in visual_qc.json.
|
|
137
148
|
"""
|
|
138
|
-
hard_max =
|
|
139
|
-
segments =
|
|
149
|
+
hard_max = CONFIG["narration_cumulative_tempo_hard_max"]
|
|
150
|
+
segments = tts_segments
|
|
140
151
|
no_safe = [
|
|
141
|
-
|
|
142
|
-
for i, s in enumerate(segments)
|
|
152
|
+
s["index"] for s in segments
|
|
143
153
|
if s.get("fit_status") == "no_safe_fit"
|
|
144
154
|
or s.get("truncate_reason") in {"no_safe_boundary", "no_room"}
|
|
145
|
-
or
|
|
146
|
-
]
|
|
147
|
-
skipped = [
|
|
148
|
-
int(s.get("index", i))
|
|
149
|
-
for i, s in enumerate(segments)
|
|
150
|
-
if s.get("fit_status") == "skipped"
|
|
151
|
-
]
|
|
152
|
-
fit_failed = [
|
|
153
|
-
int(s.get("index", i))
|
|
154
|
-
for i, s in enumerate(segments)
|
|
155
|
-
if s.get("fit_status") == "speed_adjust_failed"
|
|
155
|
+
or s.get("blocking", False)
|
|
156
156
|
]
|
|
157
|
+
skipped = [s["index"] for s in segments if s.get("fit_status") == "skipped"]
|
|
157
158
|
tempo_exceeded = []
|
|
158
159
|
truncated = []
|
|
159
160
|
handoff_failed = []
|
|
160
161
|
timeline_audio_failed = []
|
|
161
162
|
max_effective = 0.0
|
|
162
|
-
for
|
|
163
|
-
|
|
164
|
-
eff = float(s.get("effective_tempo", 0.0) or 0.0)
|
|
165
|
-
except (TypeError, ValueError):
|
|
166
|
-
eff = 0.0
|
|
163
|
+
for s in segments:
|
|
164
|
+
eff = float(s["effective_tempo"])
|
|
167
165
|
max_effective = max(max_effective, eff)
|
|
168
166
|
if eff > hard_max + 1e-6:
|
|
169
|
-
tempo_exceeded.append(
|
|
170
|
-
if
|
|
171
|
-
truncated.append(
|
|
172
|
-
if
|
|
173
|
-
handoff_failed.append(
|
|
174
|
-
if s
|
|
175
|
-
|
|
176
|
-
valid_placed_file = False
|
|
177
|
-
if placed_path and Path(placed_path).exists():
|
|
178
|
-
try:
|
|
179
|
-
with wave.open(str(placed_path), "rb") as placed_wav:
|
|
180
|
-
placed_duration = placed_wav.getnframes() / placed_wav.getframerate()
|
|
181
|
-
tolerance = 1.0 / placed_wav.getframerate()
|
|
182
|
-
timeline_start = round(float(s.get("actual_place_start")), 4)
|
|
183
|
-
timeline_end = math.ceil(
|
|
184
|
-
float(s.get("actual_place_end")) * 10_000 - 1e-9
|
|
185
|
-
) / 10_000
|
|
186
|
-
serialized_span = timeline_end - timeline_start
|
|
187
|
-
valid_placed_file = abs(
|
|
188
|
-
placed_duration - float(s.get("placed_audio_duration") or 0.0)
|
|
189
|
-
) <= tolerance + 1e-9 and serialized_span + 1e-9 >= placed_duration
|
|
190
|
-
except (OSError, wave.Error, ZeroDivisionError, TypeError, ValueError):
|
|
191
|
-
valid_placed_file = False
|
|
192
|
-
if not valid_placed_file:
|
|
193
|
-
timeline_audio_failed.append(int(s.get("index", i)))
|
|
167
|
+
tempo_exceeded.append(s["index"])
|
|
168
|
+
if s.get("truncated", False) or s.get("truncate_reason") == "tail_trim_tolerance":
|
|
169
|
+
truncated.append(s["index"])
|
|
170
|
+
if s.get("source_handoff_blocking", False):
|
|
171
|
+
handoff_failed.append(s["index"])
|
|
172
|
+
if s["placed_audio_duration"] > 0 and not _placed_audio_matches_timeline(s):
|
|
173
|
+
timeline_audio_failed.append(s["index"])
|
|
194
174
|
|
|
195
|
-
placed = [
|
|
196
|
-
float(s.get("placed_audio_duration", 0.0) or 0.0)
|
|
197
|
-
for s in segments
|
|
198
|
-
if s.get("placed_audio_duration") is not None
|
|
199
|
-
]
|
|
175
|
+
placed = [s["placed_audio_duration"] for s in segments]
|
|
200
176
|
blocking_codes = []
|
|
201
177
|
if not segments:
|
|
202
178
|
blocking_codes.append("missing_narration")
|
|
203
179
|
if skipped:
|
|
204
180
|
blocking_codes.append("skipped_segments")
|
|
205
|
-
if fit_failed:
|
|
206
|
-
blocking_codes.append("fit_failed")
|
|
207
181
|
if no_safe:
|
|
208
182
|
blocking_codes.append("no_safe_fit")
|
|
209
183
|
if tempo_exceeded:
|
|
@@ -221,7 +195,7 @@ def _build_assembly_qc(tts_segments, video_duration, *, output_path=None,
|
|
|
221
195
|
if visual_qc is not None
|
|
222
196
|
else {"present": False, "verdict": "NOT_RUN", "blocking": False, "blocking_codes": [], "summary": {}}
|
|
223
197
|
)
|
|
224
|
-
if visual_rollup
|
|
198
|
+
if visual_rollup["blocking"]:
|
|
225
199
|
blocking_codes.append("visual_qc_failed")
|
|
226
200
|
if source_has_audio is False:
|
|
227
201
|
# Not blocking: assemble can synthesize a silent original track.
|
|
@@ -242,43 +216,34 @@ def _build_assembly_qc(tts_segments, video_duration, *, output_path=None,
|
|
|
242
216
|
if output_path.exists() and output["bytes"] <= 0:
|
|
243
217
|
blocking_codes.append("empty_output")
|
|
244
218
|
|
|
245
|
-
|
|
246
|
-
|
|
219
|
+
delivery = render_delivery if render_delivery is not None else {}
|
|
220
|
+
return {
|
|
247
221
|
"schema_version": 1,
|
|
248
222
|
"artifact": ASSEMBLY_QC,
|
|
249
223
|
"verdict": "FAIL" if blocking_codes else "PASS",
|
|
250
224
|
"blocking": bool(blocking_codes),
|
|
251
225
|
"blocking_codes": blocking_codes,
|
|
252
|
-
"duration": round(float(video_duration
|
|
226
|
+
"duration": round(float(video_duration), 4),
|
|
253
227
|
"source_audio": source_audio,
|
|
254
228
|
"loudness_mode": loudness_mode or _loudness_mode(loudnorm_measurement),
|
|
255
229
|
"loudnorm_measurement": loudnorm_measurement,
|
|
256
230
|
"release_gate": {
|
|
257
231
|
"verdict": "FAIL" if blocking_codes else "PASS",
|
|
258
|
-
"visual_qc": visual_rollup
|
|
232
|
+
"visual_qc": visual_rollup["verdict"],
|
|
259
233
|
"delivery_qc": "PASS",
|
|
260
|
-
"audio_qc": "FAIL" if
|
|
261
|
-
c in blocking_codes
|
|
262
|
-
for c in (
|
|
263
|
-
"missing_narration", "skipped_segments", "fit_failed", "no_safe_fit",
|
|
264
|
-
"effective_tempo_exceeded", "empty_narration", "truncated_speech",
|
|
265
|
-
"unsafe_source_handoff",
|
|
266
|
-
"timeline_audio_mismatch",
|
|
267
|
-
)
|
|
268
|
-
) else "PASS",
|
|
234
|
+
"audio_qc": "FAIL" if _AUDIO_QC_CODES.intersection(blocking_codes) else "PASS",
|
|
269
235
|
},
|
|
270
236
|
"visual_qc": visual_rollup,
|
|
271
237
|
"delivery_qc": {
|
|
272
|
-
"video_encode_passes":
|
|
273
|
-
"reencode_reason":
|
|
274
|
-
"audio_sample_rate":
|
|
275
|
-
"final_compat_notes":
|
|
238
|
+
"video_encode_passes": delivery.get("video_encode_passes"),
|
|
239
|
+
"reencode_reason": delivery.get("reencode_reason"),
|
|
240
|
+
"audio_sample_rate": delivery.get("audio_sample_rate"),
|
|
241
|
+
"final_compat_notes": delivery.get("final_compat_notes", []),
|
|
276
242
|
},
|
|
277
243
|
"summary": {
|
|
278
244
|
"segments": len(segments),
|
|
279
245
|
"placed_segments": sum(1 for x in placed if x > 0.0),
|
|
280
246
|
"skipped_segments": skipped,
|
|
281
|
-
"fit_failed_segments": fit_failed,
|
|
282
247
|
"no_safe_fit_segments": no_safe,
|
|
283
248
|
"tempo_exceeded_segments": tempo_exceeded,
|
|
284
249
|
"max_effective_tempo": round(max_effective, 4),
|
|
@@ -288,9 +253,6 @@ def _build_assembly_qc(tts_segments, video_duration, *, output_path=None,
|
|
|
288
253
|
},
|
|
289
254
|
"output": output,
|
|
290
255
|
}
|
|
291
|
-
qc_payload["visual_rollup"] = visual_rollup
|
|
292
|
-
qc_payload["delivery_rollup"] = qc_payload["delivery_qc"]
|
|
293
|
-
return qc_payload
|
|
294
256
|
|
|
295
257
|
|
|
296
258
|
def _write_assembly_qc(work_dir, qc):
|