@oh-story/dsh 0.1.10 → 0.1.11
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 +7 -7
- 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 +1 -1
- 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
|
@@ -3,33 +3,40 @@ import base64
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
-
import shutil
|
|
7
6
|
import tempfile
|
|
8
7
|
import time
|
|
9
8
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from threading import Lock
|
|
12
11
|
|
|
13
|
-
from
|
|
12
|
+
from approved_text_policy import (
|
|
13
|
+
PRESERVE_APPROVED_TEXT_POLICY,
|
|
14
|
+
ApprovedTextDurationError,
|
|
15
|
+
archive_current_meta,
|
|
16
|
+
enforce_duration,
|
|
17
|
+
policy_name,
|
|
18
|
+
validate_required_texts,
|
|
19
|
+
write_json_atomically as _write_tts_meta_atomically,
|
|
20
|
+
)
|
|
21
|
+
from providers.fish_audio import synthesize_fish_audio
|
|
22
|
+
import providers.index_tts as index_provider
|
|
14
23
|
from lib import (
|
|
15
24
|
CONFIG,
|
|
16
25
|
_text_char_count,
|
|
17
26
|
_truncate_at_sentence,
|
|
18
|
-
|
|
27
|
+
file_identity,
|
|
19
28
|
get_video_duration,
|
|
20
29
|
log,
|
|
21
30
|
mimo_tts_api_call,
|
|
22
31
|
narration_tempo_budget,
|
|
23
32
|
run_cmd,
|
|
24
|
-
stable_hash,
|
|
25
33
|
)
|
|
26
34
|
|
|
27
35
|
# Re-exported: tests and callers reach these through voiceover, the module owns the flow.
|
|
28
36
|
from tts_audio import _maybe_normalize_tts_wav, _normalize_tts_wav_rms # noqa: F401
|
|
29
37
|
|
|
30
|
-
SUPPORTED_TTS_ENGINES = {"mimo-tts", "fish-audio"}
|
|
38
|
+
SUPPORTED_TTS_ENGINES = {"mimo-tts", "fish-audio", "index-tts"}
|
|
31
39
|
SEGMENT_AUDIO_SCHEMA_VERSION = 1
|
|
32
|
-
TTS_CACHE_VERSION = 2
|
|
33
40
|
VOICE_REFERENCE_PREP_VERSION = 1
|
|
34
41
|
_VOICE_REFERENCE_LOCK = Lock()
|
|
35
42
|
# Process-wide voice-reference state; prepared bytes are invocation-scoped.
|
|
@@ -37,12 +44,24 @@ _VOICE_REFERENCE_STATE_KEYS = (
|
|
|
37
44
|
"voice_ref_b64",
|
|
38
45
|
"voice_ref_snapshot_path",
|
|
39
46
|
"voice_ref_snapshot_signature",
|
|
40
|
-
"voice_ref_source_signature",
|
|
41
|
-
"voice_ref_fingerprint",
|
|
42
47
|
"voice_ref_snapshot_locked",
|
|
43
48
|
)
|
|
44
49
|
|
|
45
50
|
|
|
51
|
+
def authored_text_policy():
|
|
52
|
+
"""Return the explicit cacheable policy governing creative text mutation."""
|
|
53
|
+
return policy_name(CONFIG.get("preserve_approved_text", False))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def enforce_approved_text_policy(index, seg, authored_text, spoken_text, audio_duration,
|
|
57
|
+
available_duration, max_raw_duration):
|
|
58
|
+
"""Fail closed when immutable approved text cannot fit its authored window."""
|
|
59
|
+
enforce_duration(
|
|
60
|
+
index, seg, authored_text, spoken_text, audio_duration, available_duration,
|
|
61
|
+
max_raw_duration, CONFIG.get("preserve_approved_text", False), CONFIG["breath_ms"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
46
65
|
def _parse_rate_offset(rate_str):
|
|
47
66
|
"""'+5%' -> 0.05, '-3%' -> -0.03, '+0%' -> 0.0"""
|
|
48
67
|
return float(rate_str.rstrip("%")) / 100.0
|
|
@@ -103,17 +122,23 @@ def _clean_narration_text(text):
|
|
|
103
122
|
return re.sub(r'\s+', ' ', text).strip()
|
|
104
123
|
|
|
105
124
|
|
|
106
|
-
def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
107
|
-
"""合成单个 TTS 段(线程安全),支持 resume
|
|
108
|
-
prepared = _prepare_tts_segment(i, seg, narration, tts_dir, engine)
|
|
109
|
-
if prepared is None:
|
|
110
|
-
return None
|
|
111
|
-
text, output_wav, rate, pitch, cache_key = prepared
|
|
112
|
-
cached = _reuse_tts_segment_cache(i, seg, output_wav, cache_key)
|
|
113
|
-
if cached:
|
|
114
|
-
return cached
|
|
125
|
+
def _synthesize_segment(i, seg, narration, tts_dir, engine, prepared=None):
|
|
126
|
+
"""合成单个 TTS 段(线程安全),支持 resume 跳过已有文件。
|
|
115
127
|
|
|
116
|
-
|
|
128
|
+
`prepared` is the `_prepare_tts_segment` tuple of a segment synthesize_tts already probed
|
|
129
|
+
as a cache miss; passing it skips a second sidecar read + settings resolution."""
|
|
130
|
+
if prepared is None:
|
|
131
|
+
prepared = _prepare_tts_segment(i, seg, narration, tts_dir, engine)
|
|
132
|
+
if prepared is None:
|
|
133
|
+
return None
|
|
134
|
+
cached = _reuse_tts_segment_cache(i, seg, prepared[1], prepared[4], engine)
|
|
135
|
+
if cached:
|
|
136
|
+
return cached
|
|
137
|
+
text, output_wav, rate, pitch, cache_inputs = prepared
|
|
138
|
+
|
|
139
|
+
provider_receipt = _run_tts_engine(
|
|
140
|
+
engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion")
|
|
141
|
+
)
|
|
117
142
|
|
|
118
143
|
dur = get_video_duration(output_wav)
|
|
119
144
|
seg_slot = seg["end"] - seg["start"]
|
|
@@ -124,6 +149,11 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
|
124
149
|
raw_budget = available * budget["max_raw_duration_factor"]
|
|
125
150
|
truncated = False
|
|
126
151
|
truncate_reason = "none"
|
|
152
|
+
try:
|
|
153
|
+
enforce_approved_text_policy(i, seg, seg["narration"], text, dur, available, raw_budget)
|
|
154
|
+
except ApprovedTextDurationError:
|
|
155
|
+
_cleanup_partial_tts_outputs(output_wav)
|
|
156
|
+
raise
|
|
127
157
|
if dur > raw_budget and len(text) > 5:
|
|
128
158
|
chars_per_sec = _text_char_count(text) / dur
|
|
129
159
|
target_chars = max(5, int(raw_budget * chars_per_sec) - 1)
|
|
@@ -133,20 +163,24 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
|
133
163
|
text = shortened
|
|
134
164
|
truncated = True
|
|
135
165
|
truncate_reason = "sentence_boundary"
|
|
136
|
-
|
|
166
|
+
provider_receipt = _run_tts_engine(
|
|
167
|
+
engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion")
|
|
168
|
+
)
|
|
137
169
|
dur = get_video_duration(output_wav)
|
|
138
170
|
|
|
139
171
|
norm_meta = _maybe_normalize_tts_wav(output_wav)
|
|
140
172
|
if norm_meta:
|
|
141
173
|
dur = get_video_duration(output_wav)
|
|
142
|
-
_write_tts_segment_cache(output_wav,
|
|
143
|
-
truncated, truncate_reason, norm_meta)
|
|
174
|
+
_write_tts_segment_cache(output_wav, cache_inputs, text, dur, rate_offset,
|
|
175
|
+
truncated, truncate_reason, norm_meta, provider_receipt)
|
|
144
176
|
return _build_tts_segment_result(
|
|
145
|
-
i, seg, text, output_wav, dur, rate_offset, truncated, truncate_reason, norm_meta
|
|
177
|
+
i, seg, text, output_wav, dur, rate_offset, truncated, truncate_reason, norm_meta,
|
|
178
|
+
provider_receipt)
|
|
146
179
|
|
|
147
180
|
|
|
148
181
|
def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offset,
|
|
149
|
-
truncated=False, truncate_reason="none", norm_meta=None
|
|
182
|
+
truncated=False, truncate_reason="none", norm_meta=None,
|
|
183
|
+
provider_receipt=None):
|
|
150
184
|
budget = narration_tempo_budget(rate_offset)
|
|
151
185
|
authored_text = _clean_narration_text(seg["narration"])
|
|
152
186
|
resolved_truncated = truncated or text != authored_text
|
|
@@ -156,6 +190,7 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
|
|
|
156
190
|
"start": seg["start"],
|
|
157
191
|
"end": seg["end"],
|
|
158
192
|
"narration": authored_text,
|
|
193
|
+
"authored_text": seg["narration"],
|
|
159
194
|
"spoken_text": text,
|
|
160
195
|
"truncated": resolved_truncated,
|
|
161
196
|
"truncate_reason": (truncate_reason if truncate_reason != "none" else "sentence_boundary") if resolved_truncated else "none",
|
|
@@ -174,6 +209,8 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
|
|
|
174
209
|
"tts_rate_offset": rate_offset,
|
|
175
210
|
"pause_after_ms": seg.get("pause_after_ms", CONFIG["breath_ms"]),
|
|
176
211
|
"overlaps_speech": seg.get("overlaps_speech", True),
|
|
212
|
+
"authored_text_policy": authored_text_policy(),
|
|
213
|
+
"provider_receipt": provider_receipt,
|
|
177
214
|
}
|
|
178
215
|
for optional_key in ("source_start", "source_end", "source_clip_id", "emotion"):
|
|
179
216
|
if optional_key in seg:
|
|
@@ -183,13 +220,33 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
|
|
|
183
220
|
|
|
184
221
|
def _tts_failure_record(index, seg, error):
|
|
185
222
|
"""Build a user-visible failure record for partial TTS output."""
|
|
186
|
-
|
|
223
|
+
record = {
|
|
187
224
|
"index": index,
|
|
188
225
|
"start": seg["start"],
|
|
189
226
|
"end": seg["end"],
|
|
190
227
|
"text": _clean_narration_text(seg["narration"]),
|
|
191
228
|
"error": str(error),
|
|
192
229
|
}
|
|
230
|
+
if authored_text_policy() == PRESERVE_APPROVED_TEXT_POLICY:
|
|
231
|
+
record.update({"required": True, "policy": PRESERVE_APPROVED_TEXT_POLICY})
|
|
232
|
+
if isinstance(error, ApprovedTextDurationError):
|
|
233
|
+
record.update({"required": True, **error.evidence})
|
|
234
|
+
return record
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _voice_record(engine):
|
|
238
|
+
"""Which voice this run actually used: provider, model, voice id or reference audio."""
|
|
239
|
+
settings = tts_settings_payload(engine)
|
|
240
|
+
if engine == "fish-audio":
|
|
241
|
+
return {"provider": engine, "model": settings["fish_tts_model"],
|
|
242
|
+
"voice_id": settings["fish_tts_reference_id"], "reference": None}
|
|
243
|
+
if engine == "index-tts":
|
|
244
|
+
return {"provider": engine, "model": None,
|
|
245
|
+
"voice_id": settings["index_tts_voice"], "reference": None}
|
|
246
|
+
reference = settings.get("voice_ref_identity")
|
|
247
|
+
return {"provider": engine, "model": settings["mimo_tts_model"],
|
|
248
|
+
"voice_id": None if reference else settings["mimo_tts_voice"],
|
|
249
|
+
"reference": reference}
|
|
193
250
|
|
|
194
251
|
|
|
195
252
|
def _build_tts_meta(segments, engine, narration_name, failures):
|
|
@@ -197,6 +254,7 @@ def _build_tts_meta(segments, engine, narration_name, failures):
|
|
|
197
254
|
return {
|
|
198
255
|
"segments": segments,
|
|
199
256
|
"engine": engine,
|
|
257
|
+
"voice": _voice_record(engine),
|
|
200
258
|
"narration": narration_name,
|
|
201
259
|
"partial": bool(failures),
|
|
202
260
|
"failures": failures,
|
|
@@ -218,32 +276,34 @@ def synthesize_tts(narration, work_dir):
|
|
|
218
276
|
|
|
219
277
|
if not narration:
|
|
220
278
|
raise RuntimeError("narration.json 没有可配音的解说段,已中止以避免生成无解说视频")
|
|
279
|
+
validate_required_texts(
|
|
280
|
+
narration, _clean_narration_text, CONFIG.get("preserve_approved_text", False)
|
|
281
|
+
)
|
|
221
282
|
|
|
222
283
|
cache_engine = _configured_tts_engine_for_cache()
|
|
223
|
-
if cache_engine
|
|
224
|
-
|
|
225
|
-
"Fish Audio 不接受本地 VOICE_REF/--voice-ref;请改用 FISH_TTS_REFERENCE_ID"
|
|
226
|
-
)
|
|
227
|
-
# A fully cached narration needs no credential: probe every segment's sidecar
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
284
|
+
if cache_engine in {"fish-audio", "index-tts"} and voice_ref:
|
|
285
|
+
if cache_engine == "fish-audio":
|
|
286
|
+
raise RuntimeError("Fish Audio 不接受本地 VOICE_REF/--voice-ref;请改用 FISH_TTS_REFERENCE_ID")
|
|
287
|
+
raise RuntimeError("index-tts 不支持本地 VOICE_REF/--voice-ref 克隆")
|
|
288
|
+
# A fully cached narration needs no credential: probe every segment's sidecar once here;
|
|
289
|
+
# hits are final and only misses reach the workers (with their prepared inputs).
|
|
290
|
+
segments = []
|
|
291
|
+
misses = []
|
|
231
292
|
for i, seg in enumerate(narration):
|
|
232
293
|
prepared = _prepare_tts_segment(i, seg, narration, tts_dir, cache_engine)
|
|
233
294
|
if prepared is None:
|
|
234
295
|
continue
|
|
235
|
-
|
|
236
|
-
cached
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if prepared_count == 0:
|
|
296
|
+
cached = _reuse_tts_segment_cache(i, seg, prepared[1], prepared[4], cache_engine)
|
|
297
|
+
if cached:
|
|
298
|
+
segments.append(cached)
|
|
299
|
+
else:
|
|
300
|
+
misses.append((i, seg, prepared))
|
|
301
|
+
if not segments and not misses:
|
|
242
302
|
raise RuntimeError("narration.json 没有可配音的有效文本,已中止以避免生成无解说视频")
|
|
243
|
-
if not
|
|
244
|
-
|
|
303
|
+
if not misses:
|
|
304
|
+
segments.sort(key=lambda x: x["index"])
|
|
245
305
|
log(f"TTS 引擎: {cache_engine} (cache)")
|
|
246
|
-
return
|
|
306
|
+
return segments, cache_engine, []
|
|
247
307
|
|
|
248
308
|
engine = resolve_tts_engine()
|
|
249
309
|
|
|
@@ -253,15 +313,14 @@ def synthesize_tts(narration, work_dir):
|
|
|
253
313
|
|
|
254
314
|
log(f"TTS 引擎: {engine}")
|
|
255
315
|
|
|
256
|
-
segments = []
|
|
257
316
|
failures = []
|
|
258
|
-
max_workers = min(len(
|
|
317
|
+
max_workers = min(len(misses), CONFIG["tts_workers"])
|
|
259
318
|
|
|
260
319
|
try:
|
|
261
320
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
262
321
|
futures = {
|
|
263
|
-
executor.submit(_synthesize_segment, i, seg, narration, tts_dir, engine): i
|
|
264
|
-
for i, seg in
|
|
322
|
+
executor.submit(_synthesize_segment, i, seg, narration, tts_dir, engine, prepared): i
|
|
323
|
+
for i, seg, prepared in misses
|
|
265
324
|
}
|
|
266
325
|
for future in as_completed(futures):
|
|
267
326
|
try:
|
|
@@ -278,6 +337,25 @@ def synthesize_tts(narration, work_dir):
|
|
|
278
337
|
CONFIG.pop("voice_ref_snapshot_locked", None)
|
|
279
338
|
|
|
280
339
|
segments.sort(key=lambda x: x["index"])
|
|
340
|
+
approved_text_failures = [
|
|
341
|
+
failure for failure in failures
|
|
342
|
+
if failure.get("failure_kind") == "approved_text_duration_conflict"
|
|
343
|
+
]
|
|
344
|
+
if approved_text_failures:
|
|
345
|
+
first = approved_text_failures[0]
|
|
346
|
+
evidence = {
|
|
347
|
+
key: value for key, value in first.items()
|
|
348
|
+
if key not in {"text", "error", "required"}
|
|
349
|
+
}
|
|
350
|
+
if len(approved_text_failures) > 1:
|
|
351
|
+
evidence["conflict_count"] = len(approved_text_failures)
|
|
352
|
+
raise ApprovedTextDurationError(evidence)
|
|
353
|
+
if failures and authored_text_policy() == PRESERVE_APPROVED_TEXT_POLICY:
|
|
354
|
+
sample = json.dumps(failures[:3], ensure_ascii=False, sort_keys=True)
|
|
355
|
+
raise RuntimeError(
|
|
356
|
+
f"批准稿严格模式有 {len(failures)}/{len(narration)} 个必需 TTS 段失败,"
|
|
357
|
+
f"不能按部分成功交付: {sample}"
|
|
358
|
+
)
|
|
281
359
|
if failures and not CONFIG["allow_partial_tts"]:
|
|
282
360
|
sample = "; ".join(f"段 {f['index']+1}: {f['error']}" for f in failures[:3])
|
|
283
361
|
raise RuntimeError(
|
|
@@ -297,22 +375,26 @@ def synthesize_tts(narration, work_dir):
|
|
|
297
375
|
|
|
298
376
|
|
|
299
377
|
def _run_tts_engine(engine, text, output_wav, rate="+0%", pitch="+0Hz", emotion=None):
|
|
300
|
-
"""Run one TTS engine with retry and remove partial files after failures.
|
|
301
|
-
|
|
302
|
-
|
|
378
|
+
"""Run one TTS engine with retry and remove partial files after failures.
|
|
379
|
+
|
|
380
|
+
`engine` comes from resolve_tts_engine; index-tts controls were already forced to the
|
|
381
|
+
provider defaults by _prepare_tts_segment."""
|
|
303
382
|
retries = CONFIG["tts_retries"]
|
|
304
383
|
last_error = None
|
|
305
384
|
|
|
306
385
|
for attempt in range(1, retries + 1):
|
|
307
386
|
try:
|
|
308
387
|
_cleanup_partial_tts_outputs(output_wav)
|
|
388
|
+
receipt = None
|
|
309
389
|
if engine == "mimo-tts":
|
|
310
390
|
_tts_mimo(text, output_wav, rate=rate, pitch=pitch, emotion=emotion)
|
|
311
|
-
|
|
391
|
+
elif engine == "fish-audio":
|
|
312
392
|
synthesize_fish_audio(text, output_wav, rate=rate)
|
|
393
|
+
else:
|
|
394
|
+
receipt = index_provider.synthesize_configured(text, output_wav, CONFIG)
|
|
313
395
|
if get_video_duration(output_wav) <= 0:
|
|
314
396
|
raise RuntimeError(f"{engine} 输出音频时长无效")
|
|
315
|
-
return
|
|
397
|
+
return receipt
|
|
316
398
|
except Exception as exc:
|
|
317
399
|
last_error = exc
|
|
318
400
|
_cleanup_partial_tts_outputs(output_wav)
|
|
@@ -345,19 +427,23 @@ def _prepare_tts_segment(index, seg, narration, tts_dir, engine):
|
|
|
345
427
|
if not text:
|
|
346
428
|
return None
|
|
347
429
|
output_wav = tts_dir / f"narr_{index:03d}.wav"
|
|
348
|
-
if
|
|
430
|
+
if engine == "index-tts":
|
|
431
|
+
rate, pitch = index_provider.default_controls(seg)
|
|
432
|
+
elif CONFIG["tts_dynamic_params"]:
|
|
349
433
|
rate, pitch = _compute_tts_params(text, narration, index)
|
|
350
434
|
else:
|
|
351
435
|
rate, pitch = "+0%", "+0Hz"
|
|
352
|
-
|
|
353
|
-
return text, output_wav, rate, pitch,
|
|
436
|
+
cache_inputs = _tts_segment_cache_inputs(engine, index, seg, text, rate, pitch)
|
|
437
|
+
return text, output_wav, rate, pitch, cache_inputs
|
|
354
438
|
|
|
355
439
|
|
|
356
|
-
def _reuse_tts_segment_cache(index, seg, output_wav,
|
|
357
|
-
cached = _load_tts_segment_cache(output_wav,
|
|
440
|
+
def _reuse_tts_segment_cache(index, seg, output_wav, cache_inputs, engine):
|
|
441
|
+
cached = _load_tts_segment_cache(output_wav, cache_inputs)
|
|
358
442
|
if cached is None:
|
|
359
443
|
return None
|
|
360
|
-
|
|
444
|
+
if engine == "index-tts" and not index_provider.valid_cached_receipt(cached, CONFIG):
|
|
445
|
+
return None
|
|
446
|
+
# The sidecar's audio identity (size, mtime_ns) still matches the WAV that produced
|
|
361
447
|
# `audio_duration`; re-probing would be one ffprobe process per segment on every rerun.
|
|
362
448
|
log(f" 段 {index+1}: 复用已有 ({cached['audio_duration']:.1f}s)")
|
|
363
449
|
return _build_tts_segment_result(
|
|
@@ -370,13 +456,13 @@ def _reuse_tts_segment_cache(index, seg, output_wav, cache_key):
|
|
|
370
456
|
cached["truncated"],
|
|
371
457
|
cached["truncate_reason"],
|
|
372
458
|
cached["normalization"],
|
|
459
|
+
cached.get("provider_receipt"),
|
|
373
460
|
)
|
|
374
461
|
|
|
375
462
|
|
|
376
|
-
def
|
|
377
|
-
"""
|
|
463
|
+
def _tts_segment_cache_inputs(engine, index, seg, source_text, rate, pitch):
|
|
464
|
+
"""The exact inputs that make a cached segment safe to reuse (compared by equality)."""
|
|
378
465
|
payload = {
|
|
379
|
-
"version": TTS_CACHE_VERSION,
|
|
380
466
|
"engine": engine,
|
|
381
467
|
"source_text": source_text,
|
|
382
468
|
"segment_index": index,
|
|
@@ -386,49 +472,50 @@ def _tts_segment_cache_key(engine, index, seg, source_text, rate, pitch):
|
|
|
386
472
|
"rate": rate,
|
|
387
473
|
"pitch": pitch,
|
|
388
474
|
"emotion": seg.get("emotion", ""),
|
|
389
|
-
"settings":
|
|
475
|
+
"settings": tts_settings_payload(engine),
|
|
390
476
|
}
|
|
391
|
-
|
|
477
|
+
if authored_text_policy() == PRESERVE_APPROVED_TEXT_POLICY:
|
|
478
|
+
payload.update({
|
|
479
|
+
"authored_text_policy": PRESERVE_APPROVED_TEXT_POLICY,
|
|
480
|
+
"authored_text": seg["narration"],
|
|
481
|
+
"provider_text_cleanup": "clean-narration-text-v1",
|
|
482
|
+
})
|
|
483
|
+
return payload
|
|
392
484
|
|
|
393
485
|
|
|
394
|
-
def _load_tts_segment_cache(output_wav,
|
|
395
|
-
"""Return cache metadata only when the sidecar
|
|
486
|
+
def _load_tts_segment_cache(output_wav, cache_inputs):
|
|
487
|
+
"""Return cache metadata only when the sidecar's inputs and WAV identity still match."""
|
|
396
488
|
cache_path = _tts_segment_cache_path(output_wav)
|
|
397
|
-
if not output_wav.exists() or not cache_path.exists():
|
|
489
|
+
if not output_wav.exists() or output_wav.stat().st_size == 0 or not cache_path.exists():
|
|
398
490
|
return None
|
|
491
|
+
# This skill wrote the sidecar (_write_tts_segment_cache): one it cannot parse is a bug
|
|
492
|
+
# to surface. One from the earlier content-hash schema (cache_key/audio_fingerprint,
|
|
493
|
+
# no settings/audio) is a plain miss: the segment is re-synthesized once.
|
|
399
494
|
try:
|
|
400
495
|
data = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
401
|
-
except
|
|
496
|
+
except ValueError as exc:
|
|
497
|
+
raise RuntimeError(f"TTS 缓存 sidecar 损坏: {cache_path}") from exc
|
|
498
|
+
if not isinstance(data, dict) or "settings" not in data or "audio" not in data:
|
|
402
499
|
return None
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
"tts_rate_offset", "truncated", "truncate_reason", "normalization",
|
|
406
|
-
}
|
|
407
|
-
if not isinstance(data, dict) or not required <= data.keys():
|
|
408
|
-
return None
|
|
409
|
-
try:
|
|
410
|
-
audio_fingerprint = file_fingerprint(output_wav)
|
|
411
|
-
except OSError:
|
|
412
|
-
return None
|
|
413
|
-
if data["cache_key"] != cache_key or data["audio_fingerprint"] != audio_fingerprint:
|
|
414
|
-
return None
|
|
415
|
-
return data
|
|
500
|
+
stale = data["settings"] != cache_inputs or data["audio"] != file_identity(output_wav)
|
|
501
|
+
return None if stale else data
|
|
416
502
|
|
|
417
503
|
|
|
418
|
-
def _write_tts_segment_cache(output_wav,
|
|
419
|
-
truncated=False, truncate_reason="none", norm_meta=None
|
|
420
|
-
|
|
504
|
+
def _write_tts_segment_cache(output_wav, cache_inputs, spoken_text, duration, rate_offset,
|
|
505
|
+
truncated=False, truncate_reason="none", norm_meta=None,
|
|
506
|
+
provider_receipt=None):
|
|
507
|
+
"""Persist non-secret inputs and the WAV identity for safe per-segment TTS reuse."""
|
|
421
508
|
_tts_segment_cache_path(output_wav).write_text(
|
|
422
509
|
json.dumps({
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
"audio_fingerprint": file_fingerprint(output_wav),
|
|
510
|
+
"settings": cache_inputs,
|
|
511
|
+
"audio": file_identity(output_wav),
|
|
426
512
|
"spoken_text": spoken_text,
|
|
427
513
|
"audio_duration": duration,
|
|
428
514
|
"tts_rate_offset": rate_offset,
|
|
429
515
|
"truncated": truncated,
|
|
430
516
|
"truncate_reason": truncate_reason if truncated else "none",
|
|
431
517
|
"normalization": norm_meta or None,
|
|
518
|
+
"provider_receipt": provider_receipt,
|
|
432
519
|
}, ensure_ascii=False, indent=2),
|
|
433
520
|
encoding="utf-8",
|
|
434
521
|
)
|
|
@@ -443,7 +530,7 @@ def _configured_tts_engine_for_cache():
|
|
|
443
530
|
return "fish-audio"
|
|
444
531
|
if provider not in SUPPORTED_TTS_ENGINES:
|
|
445
532
|
raise RuntimeError(
|
|
446
|
-
"TTS_PROVIDER/--tts-provider 必须是 auto、mimo-tts 或
|
|
533
|
+
"TTS_PROVIDER/--tts-provider 必须是 auto、mimo-tts、fish-audio 或 index-tts"
|
|
447
534
|
)
|
|
448
535
|
return provider
|
|
449
536
|
|
|
@@ -451,6 +538,8 @@ def _configured_tts_engine_for_cache():
|
|
|
451
538
|
def resolve_tts_engine():
|
|
452
539
|
"""Resolve the selected MiMo or Fish Audio TTS engine and require its credential."""
|
|
453
540
|
engine = _configured_tts_engine_for_cache()
|
|
541
|
+
if engine == "index-tts":
|
|
542
|
+
return engine
|
|
454
543
|
if engine == "fish-audio":
|
|
455
544
|
if CONFIG["fish_api_key"]:
|
|
456
545
|
return engine
|
|
@@ -458,11 +547,11 @@ def resolve_tts_engine():
|
|
|
458
547
|
if CONFIG["mimo_tts_api_key"]:
|
|
459
548
|
return engine
|
|
460
549
|
raise RuntimeError(
|
|
461
|
-
f"没有可用的 TTS 引擎:请设置 {CONFIG['
|
|
550
|
+
f"没有可用的 TTS 引擎:请设置 {CONFIG['mimo_tts_env_var']}(MiMo TTS 需要)。"
|
|
462
551
|
)
|
|
463
552
|
|
|
464
553
|
|
|
465
|
-
def
|
|
554
|
+
def tts_settings_payload(engine):
|
|
466
555
|
"""Return non-secret TTS settings that materially affect generated audio."""
|
|
467
556
|
settings = {
|
|
468
557
|
"engine": engine,
|
|
@@ -475,7 +564,9 @@ def tts_settings_fingerprint(engine):
|
|
|
475
564
|
"tts_segment_target_rms_dbfs": CONFIG["tts_segment_target_rms_dbfs"],
|
|
476
565
|
"tts_segment_peak_limit": CONFIG["tts_segment_peak_limit"],
|
|
477
566
|
}
|
|
478
|
-
if engine == "
|
|
567
|
+
if engine == "index-tts":
|
|
568
|
+
settings.update(index_provider.cache_settings(CONFIG))
|
|
569
|
+
elif engine == "fish-audio":
|
|
479
570
|
settings.update(
|
|
480
571
|
{
|
|
481
572
|
"fish_tts_api_url": CONFIG["fish_tts_api_url"],
|
|
@@ -496,7 +587,7 @@ def tts_settings_fingerprint(engine):
|
|
|
496
587
|
if voice_ref and engine == "mimo-tts":
|
|
497
588
|
ref_path = Path(voice_ref).expanduser()
|
|
498
589
|
settings.pop("mimo_tts_voice", None) # ignored by the voiceclone API
|
|
499
|
-
settings["
|
|
590
|
+
settings["voice_ref_identity"] = _voice_reference_identity(ref_path)
|
|
500
591
|
settings["voice_ref_preparation"] = (
|
|
501
592
|
f"pcm_s16le:24000hz:mono:30s:v{VOICE_REFERENCE_PREP_VERSION}"
|
|
502
593
|
)
|
|
@@ -538,32 +629,22 @@ def _prepare_voice_reference(ref_path):
|
|
|
538
629
|
|
|
539
630
|
|
|
540
631
|
def _voice_reference_signature(ref_path):
|
|
541
|
-
"""
|
|
632
|
+
"""Identity of the reference source: resolved path plus size and mtime_ns."""
|
|
542
633
|
ref = Path(ref_path).expanduser().resolve()
|
|
543
|
-
|
|
544
|
-
return f"{ref}:{stat.st_size}:{stat.st_mtime_ns}"
|
|
634
|
+
return {"path": str(ref), **file_identity(ref)}
|
|
545
635
|
|
|
546
636
|
|
|
547
|
-
def
|
|
548
|
-
|
|
549
|
-
resolved = str(
|
|
637
|
+
def _voice_reference_identity(ref_path):
|
|
638
|
+
"""Identity for cache settings: the locked snapshot's while a run is in flight, else live."""
|
|
639
|
+
resolved = str(Path(ref_path).expanduser().resolve())
|
|
550
640
|
if (
|
|
551
641
|
CONFIG.get("voice_ref_snapshot_locked")
|
|
552
642
|
and CONFIG.get("voice_ref_b64")
|
|
553
643
|
and CONFIG.get("voice_ref_snapshot_path") == resolved
|
|
554
|
-
and CONFIG.get("
|
|
555
|
-
):
|
|
556
|
-
return CONFIG["voice_ref_fingerprint"]
|
|
557
|
-
signature = _voice_reference_signature(ref)
|
|
558
|
-
if (
|
|
559
|
-
CONFIG.get("voice_ref_source_signature") == signature
|
|
560
|
-
and CONFIG.get("voice_ref_fingerprint")
|
|
644
|
+
and CONFIG.get("voice_ref_snapshot_signature")
|
|
561
645
|
):
|
|
562
|
-
return CONFIG["
|
|
563
|
-
|
|
564
|
-
CONFIG["voice_ref_source_signature"] = signature
|
|
565
|
-
CONFIG["voice_ref_fingerprint"] = fingerprint
|
|
566
|
-
return fingerprint
|
|
646
|
+
return CONFIG["voice_ref_snapshot_signature"]
|
|
647
|
+
return _voice_reference_signature(ref_path)
|
|
567
648
|
|
|
568
649
|
|
|
569
650
|
def _cache_prepared_voice_reference(ref_path):
|
|
@@ -576,30 +657,19 @@ def _cache_prepared_voice_reference(ref_path):
|
|
|
576
657
|
and CONFIG.get("voice_ref_snapshot_path") == resolved
|
|
577
658
|
):
|
|
578
659
|
return CONFIG["voice_ref_b64"]
|
|
660
|
+
if not ref.is_file():
|
|
661
|
+
raise FileNotFoundError(f"参考音频不存在或不是文件: {ref}")
|
|
579
662
|
signature = _voice_reference_signature(ref)
|
|
580
|
-
snapshot_path = CONFIG.get("voice_ref_snapshot_path")
|
|
581
|
-
snapshot_signature = CONFIG.get("voice_ref_snapshot_signature")
|
|
582
663
|
if (
|
|
583
664
|
CONFIG.get("voice_ref_b64")
|
|
584
|
-
and
|
|
585
|
-
and
|
|
665
|
+
and CONFIG.get("voice_ref_snapshot_path") == resolved
|
|
666
|
+
and CONFIG.get("voice_ref_snapshot_signature") == signature
|
|
586
667
|
):
|
|
587
668
|
return CONFIG["voice_ref_b64"]
|
|
588
|
-
|
|
589
|
-
raise FileNotFoundError(f"参考音频不存在或不是文件: {ref}")
|
|
590
|
-
# ffmpeg and the cache fingerprint must consume the same immutable bytes. Copy first,
|
|
591
|
-
# then derive both the normalized WAV and identity from that snapshot rather than reading
|
|
592
|
-
# a caller-mutable source twice.
|
|
593
|
-
with tempfile.TemporaryDirectory(prefix="video-recap-voice-ref-snapshot-") as temp_dir:
|
|
594
|
-
snapshot = Path(temp_dir) / f"source{ref.suffix or '.audio'}"
|
|
595
|
-
shutil.copyfile(ref, snapshot)
|
|
596
|
-
fingerprint = file_fingerprint(snapshot)
|
|
597
|
-
encoded = _prepare_voice_reference(snapshot)
|
|
669
|
+
encoded = _prepare_voice_reference(ref)
|
|
598
670
|
CONFIG["voice_ref_b64"] = encoded
|
|
599
|
-
CONFIG["voice_ref_snapshot_path"] =
|
|
671
|
+
CONFIG["voice_ref_snapshot_path"] = resolved
|
|
600
672
|
CONFIG["voice_ref_snapshot_signature"] = signature
|
|
601
|
-
CONFIG["voice_ref_source_signature"] = signature
|
|
602
|
-
CONFIG["voice_ref_fingerprint"] = fingerprint
|
|
603
673
|
return encoded
|
|
604
674
|
|
|
605
675
|
|
|
@@ -640,21 +710,28 @@ def main():
|
|
|
640
710
|
description="video-voiceover: synthesize narration audio segments from narration.json.")
|
|
641
711
|
ap.add_argument("--work-dir", required=True)
|
|
642
712
|
ap.add_argument("--narration", default=None,
|
|
643
|
-
help="narration json (default: <work-dir>/narration.json
|
|
713
|
+
help="narration json (default: <work-dir>/narration.json)")
|
|
644
714
|
ap.add_argument("--mimo-voice", default=None, help="MiMo TTS voice name")
|
|
645
715
|
ap.add_argument(
|
|
646
716
|
"--tts-provider",
|
|
647
717
|
default=os.environ.get("TTS_PROVIDER", "auto"),
|
|
648
|
-
choices=["auto", "mimo-tts", "fish-audio"],
|
|
718
|
+
choices=["auto", "mimo-tts", "fish-audio", "index-tts"],
|
|
649
719
|
help="TTS provider (default: MiMo when configured, otherwise Fish Audio)",
|
|
650
720
|
)
|
|
651
721
|
ap.add_argument("--voice-ref", default=None,
|
|
652
722
|
help="reference audio (wav/mp3/etc.) for mimo-v2.5-tts-voiceclone")
|
|
653
723
|
ap.add_argument("--allow-partial-tts", action="store_true",
|
|
654
724
|
help="allow output when some narration segments fail TTS")
|
|
725
|
+
ap.add_argument(
|
|
726
|
+
"--preserve-approved-text",
|
|
727
|
+
action="store_true",
|
|
728
|
+
help="fail closed when approved narration exceeds its window; never auto-truncate/resynthesize",
|
|
729
|
+
)
|
|
655
730
|
args = ap.parse_args()
|
|
656
731
|
work_dir = Path(args.work_dir)
|
|
657
732
|
CONFIG["tts_provider"] = args.tts_provider
|
|
733
|
+
index_provider.load_private_config(CONFIG, os.environ)
|
|
734
|
+
CONFIG["preserve_approved_text"] = args.preserve_approved_text
|
|
658
735
|
CONFIG["voice_ref"] = (
|
|
659
736
|
args.voice_ref if args.voice_ref is not None else os.environ.get("VOICE_REF", "").strip()
|
|
660
737
|
)
|
|
@@ -662,8 +739,9 @@ def main():
|
|
|
662
739
|
CONFIG["mimo_tts_voice"] = args.mimo_voice
|
|
663
740
|
if args.mimo_voice and CONFIG["voice_ref"]:
|
|
664
741
|
ap.error("--mimo-voice and --voice-ref are mutually exclusive")
|
|
665
|
-
if args.tts_provider
|
|
666
|
-
ap.error("--mimo-voice
|
|
742
|
+
if args.mimo_voice and args.tts_provider in {"fish-audio", "index-tts"}:
|
|
743
|
+
ap.error("--mimo-voice is only supported by the MiMo TTS provider")
|
|
744
|
+
# A local --voice-ref with fish-audio/index-tts is rejected by synthesize_tts.
|
|
667
745
|
# Voice-reference normalization is intentionally lazy: a fully cached rerun should not
|
|
668
746
|
# invoke ffmpeg. _tts_mimo uses a process-wide lock so a fresh parallel run still converts
|
|
669
747
|
# the reference exactly once.
|
|
@@ -672,15 +750,15 @@ def main():
|
|
|
672
750
|
if args.narration:
|
|
673
751
|
narration_path = Path(args.narration)
|
|
674
752
|
else:
|
|
675
|
-
#
|
|
676
|
-
#
|
|
677
|
-
# silently override it; legacy direct-cut callers can still pass --narration explicitly.
|
|
753
|
+
# Cut mode is cut-first/narrate-second: narration.json is already on the output
|
|
754
|
+
# timeline, so the default needs no remapping.
|
|
678
755
|
narration_path = work_dir / "narration.json"
|
|
756
|
+
if CONFIG["preserve_approved_text"]:
|
|
757
|
+
archive_current_meta(work_dir / "tts_meta.json")
|
|
679
758
|
narration = json.loads(narration_path.read_text(encoding="utf-8"))
|
|
680
759
|
tts_segments, engine_used, failures = synthesize_tts(narration, work_dir)
|
|
681
760
|
meta = _build_tts_meta(tts_segments, engine_used, narration_path.name, failures)
|
|
682
|
-
(work_dir / "tts_meta.json")
|
|
683
|
-
json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
761
|
+
_write_tts_meta_atomically(work_dir / "tts_meta.json", meta)
|
|
684
762
|
if failures:
|
|
685
763
|
log(f"配音完成但缺 {len(failures)} 段:成片可预览但不建议直接发布")
|
|
686
764
|
log(f"配音完成: {len(tts_segments)} 段, 引擎 {engine_used}")
|