@oh-story/dsh 0.1.10 → 0.1.12
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 +11 -11
- package/lib/client.js +7 -3
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +212 -142
- package/lib/drama/skills/short-drama/SKILL.md +14 -2
- package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
- package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
- package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
- package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
- package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
- package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
- package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
- package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
- package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
- package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
- package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
- package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
- package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
- package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
- package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
- package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
- package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
- package/lib/index.js +71 -21
- package/lib/index.js.map +3 -3
- package/lib/oh-story/manifest.json +545 -310
- package/lib/oh-story/roles/chapter-extractor.md +11 -11
- package/lib/oh-story/roles/character-designer.md +1 -2
- package/lib/oh-story/roles/consistency-checker.md +11 -9
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +9 -61
- package/lib/oh-story/roles/story-explorer.md +20 -125
- package/lib/oh-story/roles/story-researcher.md +0 -1
- package/lib/oh-story/skills/story/SKILL.md +6 -6
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
- package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-import/SKILL.md +63 -579
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
- package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
- package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
- package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
- package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
- package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
- package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
- package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
- package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
- package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
- package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
- package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
- 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 +4 -4
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
- package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
- package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-review/SKILL.md +19 -273
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
- package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
- package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
- package/lib/oh-story/skills/story-review/references/solo.md +52 -0
- package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
- package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
- 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 +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
- package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
- package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
- package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
- package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
- package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
- package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
- package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
- package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +2 -2
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +60 -1
- package/lib/video-recap/README.en.md +348 -137
- package/lib/video-recap/README.md +340 -136
- package/lib/video-recap/manifest.json +406 -251
- package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
- package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
- package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
- package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
- package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
- package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
- package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
- package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
- package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
- package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
- package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
- package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
- package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
- package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
- package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
- package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
- package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
- package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
- package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
- package/lib/video-recap/skills/video-script/SKILL.md +16 -13
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
- package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
- package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
- package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
- package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
- package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
- package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
- package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
- package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
- package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
- package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
- package/package.json +41 -41
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
- package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
- package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
- /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
- /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
- /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
|
@@ -9,46 +9,160 @@ import assemble_constants as constants
|
|
|
9
9
|
import assembly_contract
|
|
10
10
|
import assembly_settings
|
|
11
11
|
import audio_mix
|
|
12
|
+
import adoption.audio_mix_binding as audio_mix_binding
|
|
13
|
+
import adoption.frozen_audio as frozen_audio
|
|
12
14
|
import media
|
|
13
15
|
import narration_audio
|
|
16
|
+
import adoption.narration_binding as narration_binding
|
|
17
|
+
import pair_media
|
|
14
18
|
import render_preflight
|
|
15
|
-
import
|
|
19
|
+
import adoption.strict_publish as strict_publish
|
|
20
|
+
import subtitles.render as subtitle_render
|
|
21
|
+
import subtitles.track_binding as subtitle_track_binding
|
|
16
22
|
import timeline_emit
|
|
23
|
+
import packaging
|
|
17
24
|
import visual_render
|
|
18
25
|
import lib
|
|
19
|
-
from assembly_settings import assembly_settings_fingerprint
|
|
20
|
-
from audio_mix import final_loudnorm_filter
|
|
21
26
|
|
|
22
27
|
__all__ = [
|
|
23
28
|
"assemble_video",
|
|
24
|
-
"assembly_settings_fingerprint",
|
|
25
|
-
"final_loudnorm_filter",
|
|
26
29
|
"main",
|
|
27
30
|
]
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
AUDIO_MODES = ("narration", "source-mix", "adopted-packet-copy")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_current_narration_binding = strict_publish.current_narration_binding
|
|
37
|
+
_current_audio_mix_binding = strict_publish.current_audio_mix_binding
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def assemble_video(input_video, tts_segments, work_dir, output_path, *,
|
|
41
|
+
audio_mode="narration", audio_stream_index=0,
|
|
42
|
+
narration_adoption_path=None, tts_meta_path=None,
|
|
43
|
+
audio_mix_adoption_path=None):
|
|
31
44
|
"""组装最终视频"""
|
|
32
|
-
if not
|
|
45
|
+
if audio_mode not in AUDIO_MODES:
|
|
46
|
+
raise RuntimeError(f"不支持的 audio_mode: {audio_mode}")
|
|
47
|
+
if isinstance(audio_stream_index, bool) or not isinstance(audio_stream_index, int) or audio_stream_index < 0:
|
|
48
|
+
raise RuntimeError("audio_stream_index 必须是非负整数")
|
|
49
|
+
if audio_mode == "narration" and not tts_segments:
|
|
33
50
|
raise RuntimeError("tts_meta.json 没有有效解说音频,已中止以避免生成无解说视频")
|
|
51
|
+
if audio_mode == "narration" and audio_stream_index != 0:
|
|
52
|
+
raise RuntimeError("narration 当前不支持非零 audio_stream_index")
|
|
53
|
+
if audio_mode == "source-mix" and tts_segments:
|
|
54
|
+
raise RuntimeError("source-mix 与 TTS 解说不兼容")
|
|
55
|
+
if audio_mode != "narration" and tts_meta_path is not None:
|
|
56
|
+
raise RuntimeError(f"tts_meta 与 audio_mode {audio_mode} 不兼容")
|
|
57
|
+
bgm_path = lib.CONFIG["bgm_path"]
|
|
58
|
+
has_bgm = bool(bgm_path) and os.path.exists(bgm_path)
|
|
59
|
+
if audio_mode == "source-mix" and bgm_path and not has_bgm:
|
|
60
|
+
raise RuntimeError(f"source-mix 声明的 BGM 文件不存在: {bgm_path}")
|
|
61
|
+
if (
|
|
62
|
+
audio_mode != "narration"
|
|
63
|
+
and audio_stream_index != 0
|
|
64
|
+
and lib.CONFIG["export_jianying"]
|
|
65
|
+
):
|
|
66
|
+
raise RuntimeError("剪映导出当前不支持选择非零音频流")
|
|
67
|
+
if audio_mode == "adopted-packet-copy":
|
|
68
|
+
if tts_segments:
|
|
69
|
+
raise RuntimeError("adopted-packet-copy 与 TTS 解说不兼容")
|
|
70
|
+
if bgm_path:
|
|
71
|
+
raise RuntimeError("adopted-packet-copy 与 BGM 混音不兼容")
|
|
72
|
+
if audio_mode != "narration" and narration_adoption_path is not None:
|
|
73
|
+
raise RuntimeError("narration adoption 仅适用于 narration audio_mode")
|
|
74
|
+
if audio_mix_adoption_path is not None and (
|
|
75
|
+
audio_mode != "narration" or narration_adoption_path is None or tts_meta_path is None
|
|
76
|
+
):
|
|
77
|
+
raise RuntimeError("audio mix adoption 要求 narration 模式及显式 narration adoption/tts_meta")
|
|
78
|
+
|
|
79
|
+
published_output = Path(output_path)
|
|
80
|
+
if audio_mix_adoption_path is not None and published_output.exists():
|
|
81
|
+
raise RuntimeError("显式音频混合要求新的 output_path,不能覆盖已有成片")
|
|
82
|
+
explicit_mix = (
|
|
83
|
+
audio_mix_binding.load_adoption(
|
|
84
|
+
audio_mix_adoption_path, input_video=input_video,
|
|
85
|
+
narration_adoption_path=narration_adoption_path, tts_segments=tts_segments,
|
|
86
|
+
)
|
|
87
|
+
if audio_mix_adoption_path is not None else None
|
|
88
|
+
)
|
|
34
89
|
|
|
90
|
+
binding = narration_binding.prepare_binding(
|
|
91
|
+
tts_segments, work_dir,
|
|
92
|
+
narration_adoption_path=narration_adoption_path,
|
|
93
|
+
tts_meta_path=tts_meta_path,
|
|
94
|
+
) if audio_mode == "narration" else None
|
|
95
|
+
render_output = published_output
|
|
96
|
+
if binding and binding["active"]:
|
|
97
|
+
if published_output.exists():
|
|
98
|
+
raise RuntimeError("身份约束渲染要求新的 output_path,不能覆盖已有成片")
|
|
99
|
+
render_output = published_output.with_name(
|
|
100
|
+
f".{published_output.stem}.narration-rendering{published_output.suffix}"
|
|
101
|
+
)
|
|
102
|
+
render_output.unlink(missing_ok=True)
|
|
103
|
+
output_path = render_output
|
|
35
104
|
video_duration = lib.get_video_duration(input_video)
|
|
36
105
|
canvas = media._probe_canvas(input_video) # drives subtitle PlayRes/scale so 竖屏 text isn't stretched
|
|
37
106
|
burn_subtitles = lib.CONFIG["burn_subtitles"]
|
|
107
|
+
subtitle_track_binding.prepare_subtitle_track(
|
|
108
|
+
input_video,
|
|
109
|
+
work_dir,
|
|
110
|
+
video_duration,
|
|
111
|
+
audio_mode=audio_mode,
|
|
112
|
+
selected_audio_stream=audio_stream_index,
|
|
113
|
+
)
|
|
114
|
+
adopted_source_audio = None
|
|
115
|
+
if audio_mode == "adopted-packet-copy":
|
|
116
|
+
adopted_source_audio = frozen_audio.validate_adopted_source(
|
|
117
|
+
input_video, audio_stream_index
|
|
118
|
+
)
|
|
119
|
+
if type(adopted_source_audio["sample_rate"]) is not int \
|
|
120
|
+
or adopted_source_audio["sample_rate"] <= 0:
|
|
121
|
+
raise RuntimeError("adopted audio sample rate must be a positive integer")
|
|
122
|
+
elif audio_mode == "source-mix":
|
|
123
|
+
frozen_audio.probe_audio_packets(input_video, audio_stream_index)
|
|
38
124
|
|
|
39
125
|
# 解说整体提速(可选)后,将所有 TTS 片段按时间位置合成到与视频等长的音轨上
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if handoffs:
|
|
45
|
-
lib.log(
|
|
46
|
-
"原声句末交接: "
|
|
47
|
-
+ ", ".join(
|
|
48
|
-
f"{item['end']:.2f}s→{item.get('restore_at', item['end']):.2f}s({item['status']})"
|
|
49
|
-
for item in handoffs
|
|
50
|
-
)
|
|
126
|
+
narration_wav = None
|
|
127
|
+
if audio_mode == "narration" and explicit_mix is not None:
|
|
128
|
+
explicit_runtime = audio_mix_binding.render_explicit_mix(
|
|
129
|
+
explicit_mix, binding, tts_segments, work_dir
|
|
51
130
|
)
|
|
131
|
+
narration_wav = Path(explicit_runtime["voice_bus"]["path"])
|
|
132
|
+
elif audio_mode == "narration":
|
|
133
|
+
if binding["tempo_policy"]:
|
|
134
|
+
narration_audio._apply_narration_speed(
|
|
135
|
+
tts_segments, work_dir, tempo_policy=binding["tempo_policy"]
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
narration_audio._apply_narration_speed(tts_segments, work_dir)
|
|
139
|
+
narration_wav = work_dir / "narration.wav"
|
|
140
|
+
if binding["tempo_policy"]:
|
|
141
|
+
narration_audio._build_timed_narration(
|
|
142
|
+
tts_segments, narration_wav, video_duration, work_dir,
|
|
143
|
+
tempo_policy=binding["tempo_policy"],
|
|
144
|
+
)
|
|
145
|
+
if any(
|
|
146
|
+
segment.get("blocking") or segment.get("fit_status") == "no_safe_fit"
|
|
147
|
+
for segment in tts_segments
|
|
148
|
+
):
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
"严格 narration adoption 存在 no_safe_fit,禁止提速或裁尾渲染"
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
narration_audio._build_timed_narration(
|
|
154
|
+
tts_segments, narration_wav, video_duration, work_dir
|
|
155
|
+
)
|
|
156
|
+
handoffs = audio_mix._apply_source_sentence_handoffs(tts_segments, work_dir, video_duration)
|
|
157
|
+
if handoffs:
|
|
158
|
+
lib.log(
|
|
159
|
+
"原声句末交接: "
|
|
160
|
+
+ ", ".join(
|
|
161
|
+
f"{item['end']:.2f}s→{item.get('restore_at', item['end']):.2f}s({item['status']})"
|
|
162
|
+
for item in handoffs
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
narration_binding.seal_render_inputs(binding, tts_segments, narration_wav)
|
|
52
166
|
|
|
53
167
|
# 始终生成 SRT 字幕文件(原声留白处补烧原声字幕,传入成片时长以计算留白区间)
|
|
54
168
|
srt_path = subtitle_render._generate_srt(tts_segments, work_dir, video_duration)
|
|
@@ -59,17 +173,24 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
59
173
|
lib.log(f"压制字幕文件: {ass_path}")
|
|
60
174
|
|
|
61
175
|
# 可选 BGM:作为一条独立音轨(input [2:a])混入,旁白处自动压低
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
176
|
+
if explicit_mix is not None:
|
|
177
|
+
lib.log("显式 adopted full-sound:忽略环境 BGM/duck/loudnorm/tempo 配置")
|
|
178
|
+
elif bgm_path and not has_bgm:
|
|
65
179
|
lib.log(f" ⚠️ BGM 文件不存在,跳过: {bgm_path}")
|
|
66
180
|
elif has_bgm:
|
|
67
181
|
lib.log(f"BGM 铺底: {bgm_path} (音量 {lib.CONFIG['bgm_volume']},旁白时 {lib.CONFIG['bgm_ducking_volume']})")
|
|
68
182
|
|
|
69
183
|
# 多轨时间线模型(timeline.json):canonical 渲染仍是 ffmpeg,此模型供检视/可选导出
|
|
70
|
-
timeline_emit._emit_timeline(
|
|
184
|
+
timeline_emit._emit_timeline(
|
|
185
|
+
input_video, tts_segments, work_dir, video_duration, canvas, has_bgm,
|
|
186
|
+
audio_mode=audio_mode, selected_audio_stream=audio_stream_index,
|
|
187
|
+
explicit_audio_mix=(
|
|
188
|
+
{**explicit_mix, **explicit_mix["runtime"]} if explicit_mix is not None else None
|
|
189
|
+
),
|
|
190
|
+
)
|
|
71
191
|
|
|
72
192
|
overlay_filters, overlay_qc = visual_render._visual_overlay_filters(work_dir, canvas, video_duration)
|
|
193
|
+
packaging_layers = packaging.load_packaging_layers(work_dir, canvas)
|
|
73
194
|
mask_filter = visual_render._source_subtitle_mask_filter(canvas, work_dir, tts_segments, video_duration)
|
|
74
195
|
visual_qc = visual_render._build_visual_qc(
|
|
75
196
|
tts_segments,
|
|
@@ -86,65 +207,94 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
86
207
|
codes = ", ".join(visual_qc["blocking_codes"])
|
|
87
208
|
raise RuntimeError(f"视觉 QC 失败: {codes};详见 {Path(work_dir) / constants.VISUAL_QC}")
|
|
88
209
|
|
|
89
|
-
#
|
|
210
|
+
# Select exactly one of three explicit audio paths. Only narration may synthesize
|
|
211
|
+
# a missing original track; adopted copy never decodes, mixes, normalizes or trims.
|
|
90
212
|
source_has_audio = media._has_audio_stream(input_video)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"-f", "lavfi", "-t", str(video_duration),
|
|
99
|
-
"-i", "anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
100
|
-
]
|
|
101
|
-
original_audio_label = "2:a"
|
|
102
|
-
bgm_audio_label = "3:a"
|
|
103
|
-
filter_complex = audio_mix._build_audio_filter_complex(
|
|
104
|
-
tts_segments,
|
|
105
|
-
has_bgm,
|
|
106
|
-
original_audio_label=original_audio_label,
|
|
107
|
-
bgm_audio_label=bgm_audio_label,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# BGM is input [2:a]; -stream_loop -1 loops it to cover the whole timeline (amix
|
|
111
|
-
# duration=first + -t trim it back to the video length).
|
|
112
|
-
bgm_input = ["-stream_loop", "-1", "-i", str(bgm_path)] if has_bgm else []
|
|
113
|
-
|
|
114
|
-
# 末端整体响度归一:ducking 只管相对平衡,这一步统一成片绝对响度
|
|
115
|
-
loudnorm_measurement = audio_mix._run_loudnorm_first_pass(
|
|
116
|
-
input_video,
|
|
117
|
-
narration_wav,
|
|
118
|
-
original_audio_input,
|
|
119
|
-
bgm_input,
|
|
120
|
-
filter_complex,
|
|
121
|
-
work_dir,
|
|
122
|
-
)
|
|
123
|
-
final_ln = audio_mix.final_loudnorm_filter(loudnorm_measurement)
|
|
124
|
-
filter_complex += f";[aout]{final_ln}[aoutln]"
|
|
125
|
-
lib.log(f"成片响度归一: {final_ln}")
|
|
126
|
-
|
|
127
|
-
# 对于超长 volume 表达式(多段解说),使用 -filter_complex_script 避免命令行溢出
|
|
213
|
+
adopted_audio = None
|
|
214
|
+
loudnorm_measurement = None
|
|
215
|
+
original_audio_input = []
|
|
216
|
+
bgm_input = []
|
|
217
|
+
filter_complex = None
|
|
218
|
+
filter_args = []
|
|
219
|
+
audio_input_args = []
|
|
128
220
|
fc_script = None
|
|
129
|
-
if
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
221
|
+
if audio_mode == "adopted-packet-copy":
|
|
222
|
+
audio_map = f"0:a:{audio_stream_index}"
|
|
223
|
+
elif audio_mode == "source-mix":
|
|
224
|
+
audio_map = "[aoutln]"
|
|
225
|
+
source_label = f"0:a:{audio_stream_index}"
|
|
226
|
+
filter_complex = f"[{source_label}]volume={lib.CONFIG['idle_orig_volume']}[source]"
|
|
227
|
+
if has_bgm:
|
|
228
|
+
bgm_input = ["-stream_loop", "-1", "-i", str(bgm_path)]
|
|
229
|
+
filter_complex += (
|
|
230
|
+
f";[1:a]volume={lib.CONFIG['bgm_volume']}[bgm]"
|
|
231
|
+
";[source][bgm]amix=inputs=2:duration=first:dropout_transition=0[aout]"
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
filter_complex += ";[source]anull[aout]"
|
|
235
|
+
final_ln = audio_mix.final_loudnorm_filter()
|
|
236
|
+
filter_complex += f";[aout]{final_ln}[aoutln]"
|
|
237
|
+
lib.log(f"source-mix 音频处理: source volume + {final_ln}")
|
|
238
|
+
elif explicit_mix is not None:
|
|
239
|
+
audio_map = "1:a:0"
|
|
240
|
+
audio_input_args = ["-i", explicit_mix["runtime"]["master"]["path"]]
|
|
134
241
|
else:
|
|
135
|
-
|
|
242
|
+
# 混合原始音频 + 解说音频(+ 可选 BGM)
|
|
243
|
+
if source_has_audio:
|
|
244
|
+
original_audio_label = "0:a"
|
|
245
|
+
bgm_audio_label = "2:a"
|
|
246
|
+
else:
|
|
247
|
+
lib.log("源视频无音轨,使用静音原声音轨进行混音")
|
|
248
|
+
original_audio_input = [
|
|
249
|
+
"-f", "lavfi", "-t", str(video_duration),
|
|
250
|
+
"-i", "anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
251
|
+
]
|
|
252
|
+
original_audio_label = "2:a"
|
|
253
|
+
bgm_audio_label = "3:a"
|
|
254
|
+
filter_complex = audio_mix._build_audio_filter_complex(
|
|
255
|
+
tts_segments,
|
|
256
|
+
has_bgm,
|
|
257
|
+
original_audio_label=original_audio_label,
|
|
258
|
+
bgm_audio_label=bgm_audio_label,
|
|
259
|
+
)
|
|
260
|
+
# BGM is input [2:a]; -stream_loop -1 loops it to cover the whole timeline (amix
|
|
261
|
+
# duration=first + -t trim it back to the video length).
|
|
262
|
+
bgm_input = ["-stream_loop", "-1", "-i", str(bgm_path)] if has_bgm else []
|
|
263
|
+
# 末端整体响度归一:ducking 只管相对平衡,这一步统一成片绝对响度
|
|
264
|
+
loudnorm_measurement = audio_mix._run_loudnorm_first_pass(
|
|
265
|
+
input_video,
|
|
266
|
+
narration_wav,
|
|
267
|
+
original_audio_input,
|
|
268
|
+
bgm_input,
|
|
269
|
+
filter_complex,
|
|
270
|
+
work_dir,
|
|
271
|
+
)
|
|
272
|
+
final_ln = audio_mix.final_loudnorm_filter(loudnorm_measurement)
|
|
273
|
+
filter_complex += f";[aout]{final_ln}[aoutln]"
|
|
274
|
+
lib.log(f"成片响度归一: {final_ln}")
|
|
275
|
+
audio_map = "[aoutln]"
|
|
276
|
+
audio_input_args = ["-i", str(narration_wav), *original_audio_input]
|
|
277
|
+
|
|
278
|
+
# 对于超长 volume 表达式(多段解说),从脚本文件读取 filter_complex 避免命令行溢出
|
|
279
|
+
if filter_complex is not None:
|
|
280
|
+
if len(filter_complex.encode("utf-8")) > constants.FILTER_SCRIPT_THRESHOLD_BYTES:
|
|
281
|
+
fc_script = Path(work_dir) / ".filter_complex.txt"
|
|
282
|
+
fc_script.write_text(filter_complex, encoding="utf-8")
|
|
283
|
+
lib.log(f"使用 filter_complex 脚本文件 (表达式长度 {len(filter_complex.encode('utf-8'))} bytes)")
|
|
284
|
+
filter_args = lib.filter_file_args("filter_complex", fc_script)
|
|
285
|
+
else:
|
|
286
|
+
filter_args = ["-filter_complex", filter_complex]
|
|
136
287
|
cmd = [
|
|
137
288
|
"ffmpeg", "-y",
|
|
138
289
|
"-i", str(input_video),
|
|
139
|
-
|
|
140
|
-
*original_audio_input,
|
|
290
|
+
*audio_input_args,
|
|
141
291
|
*bgm_input,
|
|
142
292
|
*filter_args,
|
|
143
293
|
# 0:v:0 (not 0:v): sources with attached cover art carry a second video stream.
|
|
144
294
|
# -vf only ever applies to the first one, so mapping all of them makes ffmpeg
|
|
145
295
|
# abort with "Could not write header (incorrect codec parameters ?)" and leave
|
|
146
296
|
# an unreadable file — after the whole pipeline has already run.
|
|
147
|
-
"-map", "0:v:0", "-map",
|
|
297
|
+
"-map", "0:v:0", "-map", audio_map,
|
|
148
298
|
]
|
|
149
299
|
|
|
150
300
|
# Video filter chain: mask source subtitles first (drawbox), then burn our subtitles
|
|
@@ -168,17 +318,19 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
168
318
|
# needs EVEN width AND height, so normalize odd dims (4:2:2/4:4:4 permit them) before the
|
|
169
319
|
# encode — otherwise libx264 aborts to a 0-byte file. The downscale helper already evens out.
|
|
170
320
|
even = "scale=trunc(iw/2)*2:trunc(ih/2)*2"
|
|
171
|
-
reencode = bool(vf_chain) or lib.CONFIG["force_video_reencode"]
|
|
321
|
+
reencode = bool(vf_chain or packaging_layers) or lib.CONFIG["force_video_reencode"]
|
|
172
322
|
notes = []
|
|
173
323
|
video_filter_script = None
|
|
174
|
-
if vf_chain:
|
|
324
|
+
if vf_chain or packaging_layers:
|
|
175
325
|
if max_h <= 0: # no downscale in the chain to force even dims
|
|
176
326
|
vf_chain.append(even)
|
|
177
|
-
video_filter =
|
|
327
|
+
video_filter = packaging.compose_video_filter(
|
|
328
|
+
vf_chain, packaging_layers, mask_first=bool(mask_filter)
|
|
329
|
+
)
|
|
178
330
|
if len(video_filter.encode("utf-8")) > constants.FILTER_SCRIPT_THRESHOLD_BYTES:
|
|
179
331
|
video_filter_script = Path(work_dir) / ".video_filter.txt"
|
|
180
332
|
video_filter_script.write_text(video_filter, encoding="utf-8")
|
|
181
|
-
cmd +=
|
|
333
|
+
cmd += lib.filter_file_args("filter:v:0", video_filter_script)
|
|
182
334
|
lib.log(
|
|
183
335
|
"使用 video filter script "
|
|
184
336
|
f"(表达式长度 {len(video_filter.encode('utf-8'))} bytes)"
|
|
@@ -187,6 +339,7 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
187
339
|
cmd += ["-vf", video_filter]
|
|
188
340
|
cmd += ["-c:v", "libx264", "-preset", preset, "-crf", crf, "-pix_fmt", "yuv420p"]
|
|
189
341
|
notes = ((["遮挡原字幕"] if mask_filter else [])
|
|
342
|
+
+ ([f"包装图层×{len(packaging_layers)}"] if packaging_layers else [])
|
|
190
343
|
+ ([f"视觉叠加×{len(overlay_filters)}"] if overlay_filters else [])
|
|
191
344
|
+ (["压制解说字幕"] if burn_subtitles else [])
|
|
192
345
|
+ ([f"缩放≤{max_h}p"] if max_h > 0 else []))
|
|
@@ -199,8 +352,18 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
199
352
|
|
|
200
353
|
# +faststart relocates the moov atom to the front so web/social players can start
|
|
201
354
|
# before the full file downloads; valid (and beneficial) on the copy path too.
|
|
202
|
-
|
|
203
|
-
|
|
355
|
+
if audio_mode == "adopted-packet-copy":
|
|
356
|
+
# No -t/-shortest: either would discard valid AAC priming or tail packets.
|
|
357
|
+
cmd += ["-c:a", "copy", "-movie_timescale",
|
|
358
|
+
str(adopted_source_audio["sample_rate"]),
|
|
359
|
+
"-movflags", "+faststart", str(output_path)]
|
|
360
|
+
elif explicit_mix is not None:
|
|
361
|
+
cmd += ["-c:a", "aac", "-b:a", "192k", "-ar", "48000",
|
|
362
|
+
"-movie_timescale", "48000", "-movflags", "+faststart",
|
|
363
|
+
str(output_path)]
|
|
364
|
+
else:
|
|
365
|
+
cmd += ["-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-movflags", "+faststart",
|
|
366
|
+
"-t", str(video_duration), str(output_path)]
|
|
204
367
|
try:
|
|
205
368
|
result = lib.run_cmd(cmd)
|
|
206
369
|
if result.returncode != 0:
|
|
@@ -212,25 +375,61 @@ def assemble_video(input_video, tts_segments, work_dir, output_path):
|
|
|
212
375
|
if video_filter_script is not None:
|
|
213
376
|
video_filter_script.unlink(missing_ok=True)
|
|
214
377
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
378
|
+
if audio_mode == "adopted-packet-copy":
|
|
379
|
+
# Either check failing means the file at the final path is unverified: never leave it.
|
|
380
|
+
try:
|
|
381
|
+
adopted_audio = frozen_audio.verify_adopted_audio(
|
|
382
|
+
input_video, output_path, audio_stream_index
|
|
383
|
+
)
|
|
384
|
+
pair_media.validate_aac_packet_interval(adopted_audio["output"])
|
|
385
|
+
except (RuntimeError, ValueError):
|
|
386
|
+
output_path.unlink(missing_ok=True)
|
|
387
|
+
raise
|
|
388
|
+
subtitle_track_binding.verify_rendered_picture(work_dir, output_path)
|
|
389
|
+
audio_operations = {
|
|
390
|
+
"narration": audio_mode == "narration",
|
|
391
|
+
"source_mix": audio_mode == "source-mix",
|
|
392
|
+
"bgm_mix": has_bgm and audio_mode != "adopted-packet-copy" and explicit_mix is None,
|
|
393
|
+
"ducking": audio_mode == "narration" and explicit_mix is None,
|
|
394
|
+
"loudness_normalization": (
|
|
395
|
+
audio_mode != "adopted-packet-copy" and explicit_mix is None
|
|
396
|
+
and lib.CONFIG["final_loudnorm"]
|
|
397
|
+
),
|
|
398
|
+
"limiter": audio_mode != "adopted-packet-copy" and explicit_mix is None,
|
|
399
|
+
"resample": audio_mode != "adopted-packet-copy",
|
|
400
|
+
"tempo": audio_mode == "narration" and explicit_mix is None,
|
|
401
|
+
"packet_copy": audio_mode == "adopted-packet-copy",
|
|
402
|
+
}
|
|
403
|
+
if explicit_mix is not None:
|
|
404
|
+
audio_operations["explicit_audio_mix"] = True
|
|
405
|
+
render_delivery = {
|
|
406
|
+
"video_encode_passes": 1 if reencode else 0,
|
|
407
|
+
"reencode_reason": notes,
|
|
408
|
+
"audio_sample_rate": (
|
|
409
|
+
adopted_audio["output"]["sample_rate"] if adopted_audio else 48000
|
|
231
410
|
),
|
|
411
|
+
"final_compat_notes": (
|
|
412
|
+
(["yuv420p"] if reencode else ["video_copy"])
|
|
413
|
+
+ (["aac_packet_copy", "faststart"] if adopted_audio else ["aac_48000", "faststart"])
|
|
414
|
+
),
|
|
415
|
+
}
|
|
416
|
+
loudness_mode = (
|
|
417
|
+
"not_run" if audio_mode == "adopted-packet-copy" else
|
|
418
|
+
"fixed_master_gain_no_loudnorm" if explicit_mix is not None else None
|
|
419
|
+
)
|
|
420
|
+
source_audio_status = "prepared_bed_adopted" if explicit_mix is not None else None
|
|
421
|
+
render_output = strict_publish.publish_render(
|
|
422
|
+
work_dir=work_dir, binding=binding, explicit_mix=explicit_mix,
|
|
423
|
+
tts_segments=tts_segments, narration_wav=narration_wav,
|
|
424
|
+
render_output=render_output, published_output=published_output,
|
|
425
|
+
audio_mode=audio_mode, audio_operations=audio_operations,
|
|
426
|
+
adopted_audio=adopted_audio, loudness_mode=loudness_mode,
|
|
427
|
+
loudnorm_measurement=loudnorm_measurement, visual_qc=visual_qc,
|
|
428
|
+
source_has_audio=source_has_audio, video_duration=video_duration,
|
|
429
|
+
render_delivery=render_delivery, source_audio_status=source_audio_status,
|
|
232
430
|
)
|
|
233
|
-
|
|
431
|
+
lib.log(f"最终视频: {render_output} ({render_output.stat().st_size / 1024 / 1024:.1f}MB)")
|
|
432
|
+
return render_output
|
|
234
433
|
|
|
235
434
|
|
|
236
435
|
def main():
|
|
@@ -241,6 +440,18 @@ def main():
|
|
|
241
440
|
ap.add_argument("video", help="source video (edited_source.mp4 in cut mode, else the original)")
|
|
242
441
|
ap.add_argument("--work-dir", required=True)
|
|
243
442
|
ap.add_argument("--tts-meta", default=None, help="tts_meta.json (default: <work-dir>/tts_meta.json)")
|
|
443
|
+
ap.add_argument("--narration-adoption", default=None,
|
|
444
|
+
help="strict narration_adoption v1 bound to an explicit --tts-meta")
|
|
445
|
+
ap.add_argument("--audio-mix-adoption", default=None,
|
|
446
|
+
help="strict audio_mix_adoption v1 for adopted prepared bed and narration")
|
|
447
|
+
ap.add_argument(
|
|
448
|
+
"--audio-mode", choices=AUDIO_MODES,
|
|
449
|
+
default="narration", help="audio path (default: narration)",
|
|
450
|
+
)
|
|
451
|
+
ap.add_argument(
|
|
452
|
+
"--audio-stream-index", type=int, default=0,
|
|
453
|
+
help="zero-based input audio stream ordinal for source/adopted modes",
|
|
454
|
+
)
|
|
244
455
|
ap.add_argument("--recap-stem", default=None, help="final recap filename stem (default: video stem)")
|
|
245
456
|
ap.add_argument("--output-dir", default=None)
|
|
246
457
|
ap.add_argument("--burn-subtitles", action=argparse.BooleanOptionalAction, default=None,
|
|
@@ -254,10 +465,12 @@ def main():
|
|
|
254
465
|
ap.add_argument("--export-jianying", action="store_true",
|
|
255
466
|
help="also export an OPTIONAL 剪映/JianYing draft from timeline.json after rendering")
|
|
256
467
|
ap.add_argument("--jianying-out", default=None, help="parent dir for the 剪映 draft (default: work-dir)")
|
|
257
|
-
ap.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
468
|
+
bundle_group = ap.add_mutually_exclusive_group()
|
|
469
|
+
bundle_group.add_argument("--jianying-bundle-media", dest="jianying_bundle_media", action="store_true",
|
|
470
|
+
help="copy media into the 剪映 draft folder (default on; portable/self-contained)")
|
|
471
|
+
bundle_group.add_argument("--jianying-no-bundle-media", dest="jianying_bundle_media", action="store_false",
|
|
472
|
+
help="do NOT copy media into the draft — reference in place (only if 剪映 can read those paths; macOS 剪映 usually cannot)")
|
|
473
|
+
ap.set_defaults(jianying_bundle_media=None)
|
|
261
474
|
args = ap.parse_args()
|
|
262
475
|
work_dir = Path(args.work_dir)
|
|
263
476
|
if args.burn_subtitles is not None:
|
|
@@ -291,38 +504,84 @@ def main():
|
|
|
291
504
|
lib.CONFIG["source_video_explicit"] = False
|
|
292
505
|
if args.export_jianying:
|
|
293
506
|
lib.CONFIG["export_jianying"] = True
|
|
294
|
-
if args.jianying_bundle_media:
|
|
295
|
-
lib.CONFIG["jianying_bundle_media"] =
|
|
296
|
-
if args.jianying_no_bundle_media:
|
|
297
|
-
lib.CONFIG["jianying_bundle_media"] = False
|
|
507
|
+
if args.jianying_bundle_media is not None:
|
|
508
|
+
lib.CONFIG["jianying_bundle_media"] = args.jianying_bundle_media
|
|
298
509
|
render_preflight._preflight_burn_subtitles() # fail before the render if burn-in is on but ffmpeg lacks libass
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
codes = ", ".join(assembly_qc["blocking_codes"])
|
|
306
|
-
raise SystemExit(f"组装 QC 阻断交付: {codes};详见 {work_dir / constants.ASSEMBLY_QC}")
|
|
510
|
+
# Argument combinations are validated once, by assemble_video.
|
|
511
|
+
tts_meta = Path(args.tts_meta) if args.tts_meta else None
|
|
512
|
+
tts_segments = []
|
|
513
|
+
if args.audio_mode == "narration":
|
|
514
|
+
tts_meta = tts_meta or work_dir / "tts_meta.json"
|
|
515
|
+
tts_segments = json.loads(tts_meta.read_text(encoding="utf-8"))["segments"]
|
|
307
516
|
stem = args.recap_stem or Path(args.video).stem
|
|
308
517
|
base = Path(args.output_dir) if args.output_dir else work_dir.parent
|
|
309
|
-
base.mkdir(parents=True, exist_ok=True)
|
|
310
518
|
final_output = assembly_contract._resolve_final_output(base, stem)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
519
|
+
if args.audio_mix_adoption is not None and final_output.exists():
|
|
520
|
+
ap.error("explicit audio mix requires a new final delivery path")
|
|
521
|
+
delivery_stage = None
|
|
522
|
+
owned_alias = None
|
|
523
|
+
output_path = work_dir / "output.mp4"
|
|
524
|
+
try:
|
|
525
|
+
assemble_video(
|
|
526
|
+
args.video, tts_segments, work_dir, output_path,
|
|
527
|
+
audio_mode=args.audio_mode, audio_stream_index=args.audio_stream_index,
|
|
528
|
+
narration_adoption_path=args.narration_adoption, tts_meta_path=tts_meta,
|
|
529
|
+
audio_mix_adoption_path=args.audio_mix_adoption,
|
|
530
|
+
)
|
|
531
|
+
assembly_qc = artifacts._load_work_json(work_dir, constants.ASSEMBLY_QC)
|
|
532
|
+
if assembly_qc["blocking"]:
|
|
533
|
+
codes = ", ".join(assembly_qc["blocking_codes"])
|
|
534
|
+
raise SystemExit(
|
|
535
|
+
f"组装 QC 阻断交付: {codes};详见 {work_dir / constants.ASSEMBLY_QC}"
|
|
536
|
+
)
|
|
537
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
538
|
+
if args.audio_mix_adoption is not None:
|
|
539
|
+
# Publish the delivery alias only when this process created it: stage a copy,
|
|
540
|
+
# hard-link it into place (fails if the alias appeared meanwhile), remember the
|
|
541
|
+
# inode, and roll back only an alias we own.
|
|
542
|
+
delivery_stage = final_output.with_name(
|
|
543
|
+
f".{final_output.name}.rendering-{os.getpid()}"
|
|
544
|
+
)
|
|
545
|
+
with output_path.open("rb") as source, delivery_stage.open("xb") as target:
|
|
546
|
+
shutil.copyfileobj(source, target)
|
|
547
|
+
target.flush()
|
|
548
|
+
os.fsync(target.fileno())
|
|
549
|
+
try:
|
|
550
|
+
os.link(delivery_stage, final_output)
|
|
551
|
+
except FileExistsError as exc:
|
|
552
|
+
raise RuntimeError("explicit audio mix delivery alias appeared during render") from exc
|
|
553
|
+
stat = final_output.stat()
|
|
554
|
+
owned_alias = (stat.st_dev, stat.st_ino)
|
|
555
|
+
delivery_stage.unlink()
|
|
556
|
+
delivery_stage = None
|
|
557
|
+
else:
|
|
558
|
+
shutil.copy2(str(output_path), str(final_output))
|
|
559
|
+
manifest = assembly_contract._assembly_manifest_payload(
|
|
560
|
+
args.video, tts_segments, work_dir, output_path,
|
|
561
|
+
tts_meta_path=tts_meta,
|
|
562
|
+
narration_input_binding=_current_narration_binding(work_dir, args.audio_mode),
|
|
563
|
+
audio_mix_binding=_current_audio_mix_binding(work_dir, args.audio_mode),
|
|
564
|
+
final_output=final_output,
|
|
565
|
+
settings_payload=assembly_settings.assembly_settings_payload,
|
|
566
|
+
audio_mode=args.audio_mode,
|
|
567
|
+
audio_stream_index=args.audio_stream_index,
|
|
568
|
+
)
|
|
569
|
+
assembly_contract._write_assembly_manifest(work_dir, manifest)
|
|
570
|
+
except BaseException:
|
|
571
|
+
if delivery_stage is not None:
|
|
572
|
+
delivery_stage.unlink(missing_ok=True)
|
|
573
|
+
if owned_alias is not None and final_output.exists():
|
|
574
|
+
stat = final_output.stat()
|
|
575
|
+
if (stat.st_dev, stat.st_ino) == owned_alias:
|
|
576
|
+
final_output.unlink()
|
|
577
|
+
raise
|
|
319
578
|
lib.log(f"组装完成: {final_output}")
|
|
320
579
|
|
|
321
580
|
# OPTIONAL, decoupled: export a 剪映 draft from the timeline (lazy import; never
|
|
322
581
|
# required by the core render path).
|
|
323
582
|
if lib.CONFIG["export_jianying"]:
|
|
324
|
-
from
|
|
325
|
-
|
|
583
|
+
from jianying.optional import maybe_export_jianying
|
|
584
|
+
maybe_export_jianying(work_dir, args.jianying_out, stem)
|
|
326
585
|
|
|
327
586
|
print(json.dumps({"status": "assembled", "output": str(final_output), "work_dir": str(work_dir)},
|
|
328
587
|
ensure_ascii=False))
|