@oh-story/dsh 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/lib/client.js +35 -14
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +165 -69
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
- package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/manifest.json +28 -28
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
- package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
- package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
- package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
- package/lib/oh-story/manifest.json +292 -172
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +56 -242
- package/lib/oh-story/roles/story-architect.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +1 -1
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/SKILL.md +13 -3
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +17 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +41 -41
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
import base64
|
|
2
3
|
import json
|
|
3
|
-
import math
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import shutil
|
|
@@ -11,8 +11,8 @@ from pathlib import Path
|
|
|
11
11
|
from threading import Lock
|
|
12
12
|
|
|
13
13
|
from fish_audio import synthesize_fish_audio
|
|
14
|
-
from lib import CONFIG
|
|
15
14
|
from lib import (
|
|
15
|
+
CONFIG,
|
|
16
16
|
_text_char_count,
|
|
17
17
|
_truncate_at_sentence,
|
|
18
18
|
file_fingerprint,
|
|
@@ -32,14 +32,20 @@ SEGMENT_AUDIO_SCHEMA_VERSION = 1
|
|
|
32
32
|
TTS_CACHE_VERSION = 2
|
|
33
33
|
VOICE_REFERENCE_PREP_VERSION = 1
|
|
34
34
|
_VOICE_REFERENCE_LOCK = Lock()
|
|
35
|
+
# Process-wide voice-reference state; prepared bytes are invocation-scoped.
|
|
36
|
+
_VOICE_REFERENCE_STATE_KEYS = (
|
|
37
|
+
"voice_ref_b64",
|
|
38
|
+
"voice_ref_snapshot_path",
|
|
39
|
+
"voice_ref_snapshot_signature",
|
|
40
|
+
"voice_ref_source_signature",
|
|
41
|
+
"voice_ref_fingerprint",
|
|
42
|
+
"voice_ref_snapshot_locked",
|
|
43
|
+
)
|
|
35
44
|
|
|
36
45
|
|
|
37
46
|
def _parse_rate_offset(rate_str):
|
|
38
47
|
"""'+5%' -> 0.05, '-3%' -> -0.03, '+0%' -> 0.0"""
|
|
39
|
-
|
|
40
|
-
if m:
|
|
41
|
-
return float(m.group(1) + m.group(2)) / 100.0
|
|
42
|
-
return 0.0
|
|
48
|
+
return float(rate_str.rstrip("%")) / 100.0
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
def _compute_tts_params(text, narration, seg_index):
|
|
@@ -74,10 +80,9 @@ def _compute_tts_params(text, narration, seg_index):
|
|
|
74
80
|
|
|
75
81
|
return rate, pitch
|
|
76
82
|
|
|
83
|
+
|
|
77
84
|
def _clean_narration_text(text):
|
|
78
85
|
"""清理解说文本中 TTS 不应读出的内容"""
|
|
79
|
-
if not text:
|
|
80
|
-
return text
|
|
81
86
|
# 移除 markdown 格式标记
|
|
82
87
|
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # **bold** → bold
|
|
83
88
|
text = re.sub(r'\*(.+?)\*', r'\1', text) # *italic* → italic
|
|
@@ -95,8 +100,7 @@ def _clean_narration_text(text):
|
|
|
95
100
|
text = re.sub(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF'
|
|
96
101
|
r'\U0001F1E0-\U0001F1FF\U00002702-\U000027B0\U0000FE00-\U0000FEFF]', '', text)
|
|
97
102
|
# 清理多余空白
|
|
98
|
-
|
|
99
|
-
return text
|
|
103
|
+
return re.sub(r'\s+', ' ', text).strip()
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
@@ -105,15 +109,15 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
|
105
109
|
if prepared is None:
|
|
106
110
|
return None
|
|
107
111
|
text, output_wav, rate, pitch, cache_key = prepared
|
|
108
|
-
cached = _reuse_tts_segment_cache(i, seg, output_wav,
|
|
112
|
+
cached = _reuse_tts_segment_cache(i, seg, output_wav, cache_key)
|
|
109
113
|
if cached:
|
|
110
114
|
return cached
|
|
111
115
|
|
|
112
116
|
_run_tts_engine(engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion"))
|
|
113
117
|
|
|
114
|
-
dur =
|
|
118
|
+
dur = get_video_duration(output_wav)
|
|
115
119
|
seg_slot = seg["end"] - seg["start"]
|
|
116
|
-
seg_pause = seg.get("pause_after_ms", CONFIG
|
|
120
|
+
seg_pause = seg.get("pause_after_ms", CONFIG["breath_ms"]) / 1000
|
|
117
121
|
available = max(0.5, seg_slot - seg_pause)
|
|
118
122
|
rate_offset = _parse_rate_offset(rate)
|
|
119
123
|
budget = narration_tempo_budget(rate_offset)
|
|
@@ -121,7 +125,7 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
|
121
125
|
truncated = False
|
|
122
126
|
truncate_reason = "none"
|
|
123
127
|
if dur > raw_budget and len(text) > 5:
|
|
124
|
-
chars_per_sec = _text_char_count(text) / dur
|
|
128
|
+
chars_per_sec = _text_char_count(text) / dur
|
|
125
129
|
target_chars = max(5, int(raw_budget * chars_per_sec) - 1)
|
|
126
130
|
shortened = _truncate_at_sentence(text, target_chars)
|
|
127
131
|
if shortened and len(shortened) >= 5 and shortened != text:
|
|
@@ -130,23 +134,22 @@ def _synthesize_segment(i, seg, narration, tts_dir, engine):
|
|
|
130
134
|
truncated = True
|
|
131
135
|
truncate_reason = "sentence_boundary"
|
|
132
136
|
_run_tts_engine(engine, text, output_wav, rate=rate, pitch=pitch, emotion=seg.get("emotion"))
|
|
133
|
-
dur =
|
|
137
|
+
dur = get_video_duration(output_wav)
|
|
134
138
|
|
|
135
139
|
norm_meta = _maybe_normalize_tts_wav(output_wav)
|
|
136
140
|
if norm_meta:
|
|
137
|
-
dur =
|
|
141
|
+
dur = get_video_duration(output_wav)
|
|
138
142
|
_write_tts_segment_cache(output_wav, cache_key, text, dur, rate_offset,
|
|
139
143
|
truncated, truncate_reason, norm_meta)
|
|
140
|
-
|
|
144
|
+
return _build_tts_segment_result(
|
|
141
145
|
i, seg, text, output_wav, dur, rate_offset, truncated, truncate_reason, norm_meta)
|
|
142
|
-
return result
|
|
143
146
|
|
|
144
147
|
|
|
145
148
|
def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offset,
|
|
146
149
|
truncated=False, truncate_reason="none", norm_meta=None):
|
|
147
150
|
budget = narration_tempo_budget(rate_offset)
|
|
148
|
-
authored_text = _clean_narration_text(
|
|
149
|
-
resolved_truncated =
|
|
151
|
+
authored_text = _clean_narration_text(seg["narration"])
|
|
152
|
+
resolved_truncated = truncated or text != authored_text
|
|
150
153
|
result = {
|
|
151
154
|
"segment_audio_schema_version": SEGMENT_AUDIO_SCHEMA_VERSION,
|
|
152
155
|
"index": index,
|
|
@@ -165,11 +168,11 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
|
|
|
165
168
|
"global_narration_speed": budget["global_narration_speed"],
|
|
166
169
|
"segment_tempo_factor": 1.0,
|
|
167
170
|
"effective_tempo": budget["global_narration_speed"] * budget["tts_rate_factor"],
|
|
168
|
-
"rms_dbfs_before":
|
|
169
|
-
"rms_dbfs_after":
|
|
170
|
-
"peak_after":
|
|
171
|
+
"rms_dbfs_before": norm_meta["rms_dbfs_before"] if norm_meta else None,
|
|
172
|
+
"rms_dbfs_after": norm_meta["rms_dbfs_after"] if norm_meta else None,
|
|
173
|
+
"peak_after": norm_meta["peak_after"] if norm_meta else None,
|
|
171
174
|
"tts_rate_offset": rate_offset,
|
|
172
|
-
"pause_after_ms": seg.get("pause_after_ms", CONFIG
|
|
175
|
+
"pause_after_ms": seg.get("pause_after_ms", CONFIG["breath_ms"]),
|
|
173
176
|
"overlaps_speech": seg.get("overlaps_speech", True),
|
|
174
177
|
}
|
|
175
178
|
for optional_key in ("source_start", "source_end", "source_clip_id", "emotion"):
|
|
@@ -181,17 +184,16 @@ def _build_tts_segment_result(index, seg, text, output_wav, duration, rate_offse
|
|
|
181
184
|
def _tts_failure_record(index, seg, error):
|
|
182
185
|
"""Build a user-visible failure record for partial TTS output."""
|
|
183
186
|
return {
|
|
184
|
-
"index":
|
|
185
|
-
"start": seg
|
|
186
|
-
"end": seg
|
|
187
|
-
"text": _clean_narration_text(
|
|
187
|
+
"index": index,
|
|
188
|
+
"start": seg["start"],
|
|
189
|
+
"end": seg["end"],
|
|
190
|
+
"text": _clean_narration_text(seg["narration"]),
|
|
188
191
|
"error": str(error),
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
|
|
192
|
-
def _build_tts_meta(segments, engine, narration_name, failures
|
|
195
|
+
def _build_tts_meta(segments, engine, narration_name, failures):
|
|
193
196
|
"""Stable tts_meta.json payload, including partial-failure visibility."""
|
|
194
|
-
failures = list(failures or [])
|
|
195
197
|
return {
|
|
196
198
|
"segments": segments,
|
|
197
199
|
"engine": engine,
|
|
@@ -201,23 +203,16 @@ def _build_tts_meta(segments, engine, narration_name, failures=None):
|
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
|
|
206
|
+
def _reset_voice_reference_state():
|
|
207
|
+
"""Drop prepared reference bytes so a reused process re-hashes the live source."""
|
|
208
|
+
for key in _VOICE_REFERENCE_STATE_KEYS:
|
|
209
|
+
CONFIG.pop(key, None)
|
|
210
|
+
|
|
211
|
+
|
|
204
212
|
def synthesize_tts(narration, work_dir):
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
voice_ref =
|
|
208
|
-
if voice_ref:
|
|
209
|
-
# Prepared bytes are invocation-scoped. Re-hash the live source once for the cache-only
|
|
210
|
-
# probe; if fresh synthesis is needed, a stable snapshot below becomes the one identity
|
|
211
|
-
# used by both the API request and the final segment cache keys.
|
|
212
|
-
for key in (
|
|
213
|
-
"voice_ref_b64",
|
|
214
|
-
"voice_ref_snapshot_path",
|
|
215
|
-
"voice_ref_snapshot_signature",
|
|
216
|
-
"voice_ref_source_signature",
|
|
217
|
-
"voice_ref_fingerprint",
|
|
218
|
-
"voice_ref_snapshot_locked",
|
|
219
|
-
):
|
|
220
|
-
CONFIG.pop(key, None)
|
|
213
|
+
"""合成解说音频(并行)。Returns (segments, engine, failures)."""
|
|
214
|
+
_reset_voice_reference_state()
|
|
215
|
+
voice_ref = CONFIG["voice_ref"]
|
|
221
216
|
tts_dir = work_dir / "tts_segments"
|
|
222
217
|
tts_dir.mkdir(exist_ok=True)
|
|
223
218
|
|
|
@@ -229,6 +224,7 @@ def synthesize_tts(narration, work_dir):
|
|
|
229
224
|
raise RuntimeError(
|
|
230
225
|
"Fish Audio 不接受本地 VOICE_REF/--voice-ref;请改用 FISH_TTS_REFERENCE_ID"
|
|
231
226
|
)
|
|
227
|
+
# A fully cached narration needs no credential: probe every segment's sidecar first.
|
|
232
228
|
cached_segments = []
|
|
233
229
|
needs_fresh = False
|
|
234
230
|
prepared_count = 0
|
|
@@ -237,8 +233,7 @@ def synthesize_tts(narration, work_dir):
|
|
|
237
233
|
if prepared is None:
|
|
238
234
|
continue
|
|
239
235
|
prepared_count += 1
|
|
240
|
-
|
|
241
|
-
cached = _reuse_tts_segment_cache(i, seg, output_wav, text, rate, cache_key)
|
|
236
|
+
cached = _reuse_tts_segment_cache(i, seg, prepared[1], prepared[4])
|
|
242
237
|
if not cached:
|
|
243
238
|
needs_fresh = True
|
|
244
239
|
break
|
|
@@ -248,8 +243,7 @@ def synthesize_tts(narration, work_dir):
|
|
|
248
243
|
if not needs_fresh:
|
|
249
244
|
cached_segments.sort(key=lambda x: x["index"])
|
|
250
245
|
log(f"TTS 引擎: {cache_engine} (cache)")
|
|
251
|
-
|
|
252
|
-
return cached_segments, cache_engine
|
|
246
|
+
return cached_segments, cache_engine, []
|
|
253
247
|
|
|
254
248
|
engine = resolve_tts_engine()
|
|
255
249
|
|
|
@@ -261,7 +255,7 @@ def synthesize_tts(narration, work_dir):
|
|
|
261
255
|
|
|
262
256
|
segments = []
|
|
263
257
|
failures = []
|
|
264
|
-
max_workers =
|
|
258
|
+
max_workers = min(len(narration), CONFIG["tts_workers"])
|
|
265
259
|
|
|
266
260
|
try:
|
|
267
261
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
@@ -284,13 +278,12 @@ def synthesize_tts(narration, work_dir):
|
|
|
284
278
|
CONFIG.pop("voice_ref_snapshot_locked", None)
|
|
285
279
|
|
|
286
280
|
segments.sort(key=lambda x: x["index"])
|
|
287
|
-
if failures and not CONFIG
|
|
281
|
+
if failures and not CONFIG["allow_partial_tts"]:
|
|
288
282
|
sample = "; ".join(f"段 {f['index']+1}: {f['error']}" for f in failures[:3])
|
|
289
283
|
raise RuntimeError(
|
|
290
284
|
f"TTS 失败 {len(failures)}/{len(narration)} 段,已中止以避免生成缺解说的视频。"
|
|
291
285
|
f"示例: {sample}。如确需继续,可设置 ALLOW_PARTIAL_TTS=1 或 --allow-partial-tts。"
|
|
292
286
|
)
|
|
293
|
-
synthesize_tts.last_failures = failures
|
|
294
287
|
if failures:
|
|
295
288
|
missing = ", ".join(str(f["index"] + 1) for f in failures[:8])
|
|
296
289
|
more = "…" if len(failures) > 8 else ""
|
|
@@ -300,12 +293,14 @@ def synthesize_tts(narration, work_dir):
|
|
|
300
293
|
)
|
|
301
294
|
if not segments:
|
|
302
295
|
raise RuntimeError("TTS 没有生成任何有效解说音频,已中止以避免生成无解说视频")
|
|
303
|
-
return segments, engine
|
|
296
|
+
return segments, engine, failures
|
|
304
297
|
|
|
305
298
|
|
|
306
299
|
def _run_tts_engine(engine, text, output_wav, rate="+0%", pitch="+0Hz", emotion=None):
|
|
307
300
|
"""Run one TTS engine with retry and remove partial files after failures."""
|
|
308
|
-
|
|
301
|
+
if engine not in SUPPORTED_TTS_ENGINES:
|
|
302
|
+
raise RuntimeError(f"不支持的 TTS 引擎: {engine}。当前支持 mimo-tts、fish-audio。")
|
|
303
|
+
retries = CONFIG["tts_retries"]
|
|
309
304
|
last_error = None
|
|
310
305
|
|
|
311
306
|
for attempt in range(1, retries + 1):
|
|
@@ -313,17 +308,9 @@ def _run_tts_engine(engine, text, output_wav, rate="+0%", pitch="+0Hz", emotion=
|
|
|
313
308
|
_cleanup_partial_tts_outputs(output_wav)
|
|
314
309
|
if engine == "mimo-tts":
|
|
315
310
|
_tts_mimo(text, output_wav, rate=rate, pitch=pitch, emotion=emotion)
|
|
316
|
-
elif engine == "fish-audio":
|
|
317
|
-
synthesize_fish_audio(
|
|
318
|
-
text, output_wav, rate=rate, pitch=pitch, emotion=emotion
|
|
319
|
-
)
|
|
320
311
|
else:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
dur = _get_audio_duration(output_wav)
|
|
326
|
-
if dur <= 0:
|
|
312
|
+
synthesize_fish_audio(text, output_wav, rate=rate)
|
|
313
|
+
if get_video_duration(output_wav) <= 0:
|
|
327
314
|
raise RuntimeError(f"{engine} 输出音频时长无效")
|
|
328
315
|
return
|
|
329
316
|
except Exception as exc:
|
|
@@ -340,26 +327,13 @@ def _run_tts_engine(engine, text, output_wav, rate="+0%", pitch="+0Hz", emotion=
|
|
|
340
327
|
def _cleanup_partial_tts_outputs(output_wav):
|
|
341
328
|
"""Remove stale partial media files before/after a failed TTS attempt."""
|
|
342
329
|
wav_path = Path(output_wav)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
except OSError:
|
|
351
|
-
pass
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def _finite_positive(value):
|
|
355
|
-
"""Return value as a positive float, or None for missing/malformed/non-positive input."""
|
|
356
|
-
try:
|
|
357
|
-
number = float(value)
|
|
358
|
-
except (TypeError, ValueError):
|
|
359
|
-
return None
|
|
360
|
-
if not math.isfinite(number) or number <= 0:
|
|
361
|
-
return None
|
|
362
|
-
return number
|
|
330
|
+
for path in (
|
|
331
|
+
wav_path,
|
|
332
|
+
wav_path.with_suffix(".mp3"),
|
|
333
|
+
Path(str(wav_path) + ".part"),
|
|
334
|
+
_tts_segment_cache_path(output_wav),
|
|
335
|
+
):
|
|
336
|
+
path.unlink(missing_ok=True)
|
|
363
337
|
|
|
364
338
|
|
|
365
339
|
def _tts_segment_cache_path(output_wav):
|
|
@@ -368,10 +342,10 @@ def _tts_segment_cache_path(output_wav):
|
|
|
368
342
|
|
|
369
343
|
def _prepare_tts_segment(index, seg, narration, tts_dir, engine):
|
|
370
344
|
text = _clean_narration_text(seg["narration"])
|
|
371
|
-
if not text
|
|
345
|
+
if not text:
|
|
372
346
|
return None
|
|
373
347
|
output_wav = tts_dir / f"narr_{index:03d}.wav"
|
|
374
|
-
if CONFIG
|
|
348
|
+
if CONFIG["tts_dynamic_params"]:
|
|
375
349
|
rate, pitch = _compute_tts_params(text, narration, index)
|
|
376
350
|
else:
|
|
377
351
|
rate, pitch = "+0%", "+0Hz"
|
|
@@ -379,48 +353,39 @@ def _prepare_tts_segment(index, seg, narration, tts_dir, engine):
|
|
|
379
353
|
return text, output_wav, rate, pitch, cache_key
|
|
380
354
|
|
|
381
355
|
|
|
382
|
-
def _reuse_tts_segment_cache(index, seg, output_wav,
|
|
356
|
+
def _reuse_tts_segment_cache(index, seg, output_wav, cache_key):
|
|
383
357
|
cached = _load_tts_segment_cache(output_wav, cache_key)
|
|
384
|
-
if
|
|
358
|
+
if cached is None:
|
|
385
359
|
return None
|
|
386
360
|
# The sidecar's audio_fingerprint already proved these exact bytes are what produced
|
|
387
|
-
# `audio_duration
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
norm_meta = cached.get("normalization") if isinstance(cached.get("normalization"), dict) else None
|
|
401
|
-
log(f" 段 {index+1}: 复用已有 ({existing_dur:.1f}s)")
|
|
402
|
-
return _build_tts_segment_result(index, seg, spoken_text, output_wav, existing_dur,
|
|
403
|
-
rate_offset, truncated, truncate_reason, norm_meta)
|
|
361
|
+
# `audio_duration`; re-probing would be one ffprobe process per segment on every rerun.
|
|
362
|
+
log(f" 段 {index+1}: 复用已有 ({cached['audio_duration']:.1f}s)")
|
|
363
|
+
return _build_tts_segment_result(
|
|
364
|
+
index,
|
|
365
|
+
seg,
|
|
366
|
+
cached["spoken_text"],
|
|
367
|
+
output_wav,
|
|
368
|
+
cached["audio_duration"],
|
|
369
|
+
cached["tts_rate_offset"],
|
|
370
|
+
cached["truncated"],
|
|
371
|
+
cached["truncate_reason"],
|
|
372
|
+
cached["normalization"],
|
|
373
|
+
)
|
|
404
374
|
|
|
405
375
|
|
|
406
376
|
def _tts_segment_cache_key(engine, index, seg, source_text, rate, pitch):
|
|
407
377
|
"""Fingerprint the exact inputs that make a cached segment safe to reuse."""
|
|
408
|
-
pause = seg.get("pause_after_ms", CONFIG.get("breath_ms", 250))
|
|
409
|
-
try:
|
|
410
|
-
pause = int(pause)
|
|
411
|
-
except (TypeError, ValueError):
|
|
412
|
-
pause = CONFIG.get("breath_ms", 250)
|
|
413
378
|
payload = {
|
|
414
379
|
"version": TTS_CACHE_VERSION,
|
|
415
380
|
"engine": engine,
|
|
416
381
|
"source_text": source_text,
|
|
417
|
-
"segment_index":
|
|
418
|
-
"start": round(
|
|
419
|
-
"end": round(
|
|
420
|
-
"pause_after_ms":
|
|
382
|
+
"segment_index": index,
|
|
383
|
+
"start": round(seg["start"], 3),
|
|
384
|
+
"end": round(seg["end"], 3),
|
|
385
|
+
"pause_after_ms": seg.get("pause_after_ms", CONFIG["breath_ms"]),
|
|
421
386
|
"rate": rate,
|
|
422
387
|
"pitch": pitch,
|
|
423
|
-
"emotion":
|
|
388
|
+
"emotion": seg.get("emotion", ""),
|
|
424
389
|
"settings": tts_settings_fingerprint(engine),
|
|
425
390
|
}
|
|
426
391
|
return stable_hash(payload)
|
|
@@ -428,54 +393,52 @@ def _tts_segment_cache_key(engine, index, seg, source_text, rate, pitch):
|
|
|
428
393
|
|
|
429
394
|
def _load_tts_segment_cache(output_wav, cache_key):
|
|
430
395
|
"""Return cache metadata only when the sidecar proves the WAV matches narration."""
|
|
431
|
-
if not output_wav.exists():
|
|
432
|
-
return None
|
|
433
396
|
cache_path = _tts_segment_cache_path(output_wav)
|
|
434
|
-
if not cache_path.exists():
|
|
397
|
+
if not output_wav.exists() or not cache_path.exists():
|
|
435
398
|
return None
|
|
436
399
|
try:
|
|
437
|
-
import json
|
|
438
400
|
data = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
439
401
|
except (OSError, ValueError):
|
|
440
402
|
return None
|
|
441
|
-
|
|
403
|
+
required = {
|
|
404
|
+
"cache_key", "audio_fingerprint", "spoken_text", "audio_duration",
|
|
405
|
+
"tts_rate_offset", "truncated", "truncate_reason", "normalization",
|
|
406
|
+
}
|
|
407
|
+
if not isinstance(data, dict) or not required <= data.keys():
|
|
442
408
|
return None
|
|
443
409
|
try:
|
|
444
|
-
|
|
445
|
-
return None
|
|
410
|
+
audio_fingerprint = file_fingerprint(output_wav)
|
|
446
411
|
except OSError:
|
|
447
412
|
return None
|
|
413
|
+
if data["cache_key"] != cache_key or data["audio_fingerprint"] != audio_fingerprint:
|
|
414
|
+
return None
|
|
448
415
|
return data
|
|
449
416
|
|
|
450
417
|
|
|
451
418
|
def _write_tts_segment_cache(output_wav, cache_key, spoken_text, duration, rate_offset,
|
|
452
419
|
truncated=False, truncate_reason="none", norm_meta=None):
|
|
453
420
|
"""Persist non-secret provenance for safe per-segment TTS reuse."""
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
encoding="utf-8",
|
|
469
|
-
)
|
|
470
|
-
except OSError as exc:
|
|
471
|
-
log(f" TTS 缓存元数据写入失败(忽略): {exc}")
|
|
421
|
+
_tts_segment_cache_path(output_wav).write_text(
|
|
422
|
+
json.dumps({
|
|
423
|
+
"version": TTS_CACHE_VERSION,
|
|
424
|
+
"cache_key": cache_key,
|
|
425
|
+
"audio_fingerprint": file_fingerprint(output_wav),
|
|
426
|
+
"spoken_text": spoken_text,
|
|
427
|
+
"audio_duration": duration,
|
|
428
|
+
"tts_rate_offset": rate_offset,
|
|
429
|
+
"truncated": truncated,
|
|
430
|
+
"truncate_reason": truncate_reason if truncated else "none",
|
|
431
|
+
"normalization": norm_meta or None,
|
|
432
|
+
}, ensure_ascii=False, indent=2),
|
|
433
|
+
encoding="utf-8",
|
|
434
|
+
)
|
|
472
435
|
|
|
473
436
|
|
|
474
437
|
def _configured_tts_engine_for_cache():
|
|
475
438
|
"""Resolve provider intent without requiring a live credential for cache probes."""
|
|
476
|
-
provider =
|
|
439
|
+
provider = CONFIG["tts_provider"]
|
|
477
440
|
if provider == "auto":
|
|
478
|
-
if CONFIG
|
|
441
|
+
if CONFIG["mimo_tts_api_key"] or not CONFIG["fish_api_key"]:
|
|
479
442
|
return "mimo-tts"
|
|
480
443
|
return "fish-audio"
|
|
481
444
|
if provider not in SUPPORTED_TTS_ENGINES:
|
|
@@ -485,71 +448,52 @@ def _configured_tts_engine_for_cache():
|
|
|
485
448
|
return provider
|
|
486
449
|
|
|
487
450
|
|
|
488
|
-
def
|
|
489
|
-
"""
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
def _require_tts_credential(engine):
|
|
494
|
-
"""Require the credential for an already-resolved TTS provider."""
|
|
495
|
-
if engine == "mimo-tts" and CONFIG.get("mimo_tts_api_key"):
|
|
496
|
-
return engine
|
|
497
|
-
if engine == "fish-audio" and CONFIG.get("fish_api_key"):
|
|
498
|
-
return engine
|
|
451
|
+
def resolve_tts_engine():
|
|
452
|
+
"""Resolve the selected MiMo or Fish Audio TTS engine and require its credential."""
|
|
453
|
+
engine = _configured_tts_engine_for_cache()
|
|
499
454
|
if engine == "fish-audio":
|
|
455
|
+
if CONFIG["fish_api_key"]:
|
|
456
|
+
return engine
|
|
500
457
|
raise RuntimeError("没有可用的 TTS 引擎:请设置 FISH_API_KEY(Fish Audio 需要)。")
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"""Resolve the selected MiMo or Fish Audio TTS engine.
|
|
507
|
-
|
|
508
|
-
`prefer_existing` lets an assemble-only rerun reuse already-generated audio
|
|
509
|
-
even when no fresh provider credential is configured.
|
|
510
|
-
"""
|
|
511
|
-
selected = _configured_tts_engine_for_cache()
|
|
512
|
-
try:
|
|
513
|
-
return _require_tts_credential(selected)
|
|
514
|
-
except RuntimeError:
|
|
515
|
-
if prefer_existing == selected:
|
|
516
|
-
return prefer_existing
|
|
517
|
-
raise
|
|
458
|
+
if CONFIG["mimo_tts_api_key"]:
|
|
459
|
+
return engine
|
|
460
|
+
raise RuntimeError(
|
|
461
|
+
f"没有可用的 TTS 引擎:请设置 {CONFIG['mimo_tts_api_key_source']}(MiMo TTS 需要)。"
|
|
462
|
+
)
|
|
518
463
|
|
|
519
464
|
|
|
520
|
-
def tts_settings_fingerprint(engine
|
|
465
|
+
def tts_settings_fingerprint(engine):
|
|
521
466
|
"""Return non-secret TTS settings that materially affect generated audio."""
|
|
522
|
-
resolved = engine or resolve_tts_engine()
|
|
523
467
|
settings = {
|
|
524
|
-
"engine":
|
|
525
|
-
"tts_dynamic_params":
|
|
526
|
-
"narration_speed":
|
|
527
|
-
"narration_cumulative_tempo_max":
|
|
528
|
-
"narration_cumulative_tempo_hard_max":
|
|
529
|
-
"tts_segment_tempo_max":
|
|
530
|
-
"tts_segment_normalize":
|
|
531
|
-
"tts_segment_target_rms_dbfs":
|
|
532
|
-
"tts_segment_peak_limit":
|
|
468
|
+
"engine": engine,
|
|
469
|
+
"tts_dynamic_params": CONFIG["tts_dynamic_params"],
|
|
470
|
+
"narration_speed": CONFIG["narration_speed"],
|
|
471
|
+
"narration_cumulative_tempo_max": CONFIG["narration_cumulative_tempo_max"],
|
|
472
|
+
"narration_cumulative_tempo_hard_max": CONFIG["narration_cumulative_tempo_hard_max"],
|
|
473
|
+
"tts_segment_tempo_max": CONFIG["tts_segment_tempo_max"],
|
|
474
|
+
"tts_segment_normalize": CONFIG["tts_segment_normalize"],
|
|
475
|
+
"tts_segment_target_rms_dbfs": CONFIG["tts_segment_target_rms_dbfs"],
|
|
476
|
+
"tts_segment_peak_limit": CONFIG["tts_segment_peak_limit"],
|
|
533
477
|
}
|
|
534
|
-
if
|
|
478
|
+
if engine == "fish-audio":
|
|
535
479
|
settings.update(
|
|
536
480
|
{
|
|
537
|
-
"fish_tts_api_url": CONFIG
|
|
538
|
-
"fish_tts_model": CONFIG
|
|
539
|
-
"fish_tts_reference_id": CONFIG
|
|
481
|
+
"fish_tts_api_url": CONFIG["fish_tts_api_url"],
|
|
482
|
+
"fish_tts_model": CONFIG["fish_tts_model"],
|
|
483
|
+
"fish_tts_reference_id": CONFIG["fish_tts_reference_id"],
|
|
540
484
|
}
|
|
541
485
|
)
|
|
542
486
|
else:
|
|
543
487
|
settings.update(
|
|
544
488
|
{
|
|
545
|
-
"mimo_tts_api_url": CONFIG
|
|
546
|
-
"mimo_tts_model": CONFIG
|
|
547
|
-
"mimo_tts_voice": CONFIG
|
|
548
|
-
"mimo_tts_style": CONFIG
|
|
489
|
+
"mimo_tts_api_url": CONFIG["mimo_tts_api_url"],
|
|
490
|
+
"mimo_tts_model": CONFIG["mimo_tts_model"],
|
|
491
|
+
"mimo_tts_voice": CONFIG["mimo_tts_voice"],
|
|
492
|
+
"mimo_tts_style": CONFIG["mimo_tts_style"],
|
|
549
493
|
}
|
|
550
494
|
)
|
|
551
|
-
voice_ref =
|
|
552
|
-
if voice_ref and
|
|
495
|
+
voice_ref = CONFIG["voice_ref"]
|
|
496
|
+
if voice_ref and engine == "mimo-tts":
|
|
553
497
|
ref_path = Path(voice_ref).expanduser()
|
|
554
498
|
settings.pop("mimo_tts_voice", None) # ignored by the voiceclone API
|
|
555
499
|
settings["voice_ref_fingerprint"] = _voice_reference_fingerprint(ref_path)
|
|
@@ -561,10 +505,9 @@ def tts_settings_fingerprint(engine=None):
|
|
|
561
505
|
|
|
562
506
|
|
|
563
507
|
def _mimo_tts_style_instruction(rate="+0%", pitch="+0Hz", emotion=None):
|
|
564
|
-
style = CONFIG
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
tone = f"用「{emo}」的情绪和语气演绎这句解说,代入感强、有起伏,不要平铺直叙。"
|
|
508
|
+
style = CONFIG["mimo_tts_style"]
|
|
509
|
+
if emotion:
|
|
510
|
+
tone = f"用「{emotion}」的情绪和语气演绎这句解说,代入感强、有起伏,不要平铺直叙。"
|
|
568
511
|
else:
|
|
569
512
|
tone = "语气有感染力、有起伏,像在给观众讲故事,不要平淡机械。"
|
|
570
513
|
rate_offset = _parse_rate_offset(rate)
|
|
@@ -574,7 +517,7 @@ def _mimo_tts_style_instruction(rate="+0%", pitch="+0Hz", emotion=None):
|
|
|
574
517
|
speed = "语速略慢,适当停顿,保留收束感。"
|
|
575
518
|
else:
|
|
576
519
|
speed = "语速中等,节奏稳定。"
|
|
577
|
-
pitch_hint = "疑问句或情绪抬升处可自然微升调。" if pitch
|
|
520
|
+
pitch_hint = "疑问句或情绪抬升处可自然微升调。" if pitch != "+0Hz" else "音调自然。"
|
|
578
521
|
return f"{style} {tone} {speed} {pitch_hint}"
|
|
579
522
|
|
|
580
523
|
|
|
@@ -590,8 +533,7 @@ def _prepare_voice_reference(ref_path):
|
|
|
590
533
|
"-t", "30", "-acodec", "pcm_s16le", str(normalized),
|
|
591
534
|
])
|
|
592
535
|
if result.returncode != 0 or not normalized.is_file() or normalized.stat().st_size <= 44:
|
|
593
|
-
|
|
594
|
-
raise RuntimeError(f"参考音频转码失败: {detail or ref}")
|
|
536
|
+
raise RuntimeError(f"参考音频转码失败: {result.stderr.strip() or ref}")
|
|
595
537
|
return base64.b64encode(normalized.read_bytes()).decode("ascii")
|
|
596
538
|
|
|
597
539
|
|
|
@@ -604,8 +546,6 @@ def _voice_reference_signature(ref_path):
|
|
|
604
546
|
|
|
605
547
|
def _voice_reference_fingerprint(ref_path):
|
|
606
548
|
ref = Path(ref_path).expanduser()
|
|
607
|
-
if not ref.is_file():
|
|
608
|
-
return f"missing:{ref.resolve()}"
|
|
609
549
|
resolved = str(ref.resolve())
|
|
610
550
|
if (
|
|
611
551
|
CONFIG.get("voice_ref_snapshot_locked")
|
|
@@ -639,16 +579,10 @@ def _cache_prepared_voice_reference(ref_path):
|
|
|
639
579
|
signature = _voice_reference_signature(ref)
|
|
640
580
|
snapshot_path = CONFIG.get("voice_ref_snapshot_path")
|
|
641
581
|
snapshot_signature = CONFIG.get("voice_ref_snapshot_signature")
|
|
642
|
-
legacy_cache_valid = (
|
|
643
|
-
snapshot_path is None
|
|
644
|
-
and CONFIG.get("voice_ref_source_signature") == signature
|
|
645
|
-
)
|
|
646
582
|
if (
|
|
647
583
|
CONFIG.get("voice_ref_b64")
|
|
648
|
-
and
|
|
649
|
-
|
|
650
|
-
or legacy_cache_valid
|
|
651
|
-
)
|
|
584
|
+
and snapshot_path == resolved
|
|
585
|
+
and snapshot_signature == signature
|
|
652
586
|
):
|
|
653
587
|
return CONFIG["voice_ref_b64"]
|
|
654
588
|
if not ref.is_file():
|
|
@@ -674,12 +608,10 @@ def _tts_mimo(text, output_path, rate="+0%", pitch="+0Hz", emotion=None):
|
|
|
674
608
|
|
|
675
609
|
MiMo-v2.5-tts 是 instruct-TTS:user 消息里的自然语言指令控制整句的情绪/语气/语速。
|
|
676
610
|
每段 narration 的 `emotion` 标签即写进该指令,让解说有起伏、不机械。"""
|
|
677
|
-
voice_ref =
|
|
678
|
-
voice_ref_b64 = None
|
|
679
|
-
if voice_ref:
|
|
680
|
-
voice_ref_b64 = _cache_prepared_voice_reference(voice_ref)
|
|
611
|
+
voice_ref = CONFIG["voice_ref"]
|
|
612
|
+
voice_ref_b64 = _cache_prepared_voice_reference(voice_ref) if voice_ref else None
|
|
681
613
|
payload = {
|
|
682
|
-
"model": "mimo-v2.5-tts-voiceclone" if voice_ref else CONFIG
|
|
614
|
+
"model": "mimo-v2.5-tts-voiceclone" if voice_ref else CONFIG["mimo_tts_model"],
|
|
683
615
|
"messages": [
|
|
684
616
|
{"role": "user", "content": _mimo_tts_style_instruction(rate, pitch, emotion)},
|
|
685
617
|
{"role": "assistant", "content": text},
|
|
@@ -688,7 +620,7 @@ def _tts_mimo(text, output_path, rate="+0%", pitch="+0Hz", emotion=None):
|
|
|
688
620
|
"format": "wav",
|
|
689
621
|
"voice": (
|
|
690
622
|
f"data:audio/wav;base64,{voice_ref_b64}"
|
|
691
|
-
if voice_ref else CONFIG
|
|
623
|
+
if voice_ref else CONFIG["mimo_tts_voice"]
|
|
692
624
|
),
|
|
693
625
|
},
|
|
694
626
|
}
|
|
@@ -703,17 +635,7 @@ def _tts_mimo(text, output_path, rate="+0%", pitch="+0Hz", emotion=None):
|
|
|
703
635
|
raise RuntimeError("MiMo-TTS 返回的 audio.data 不是有效 base64") from exc
|
|
704
636
|
|
|
705
637
|
|
|
706
|
-
def _get_audio_duration(audio_path):
|
|
707
|
-
"""获取音频文件时长(复用 common.get_video_duration 的 ffprobe 探测)。"""
|
|
708
|
-
return get_video_duration(audio_path)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
# ── Step 7: 视频组装 ─────────────────────────────────────────────────
|
|
712
|
-
|
|
713
|
-
|
|
714
638
|
def main():
|
|
715
|
-
import argparse
|
|
716
|
-
from pathlib import Path
|
|
717
639
|
ap = argparse.ArgumentParser(
|
|
718
640
|
description="video-voiceover: synthesize narration audio segments from narration.json.")
|
|
719
641
|
ap.add_argument("--work-dir", required=True)
|
|
@@ -733,24 +655,14 @@ def main():
|
|
|
733
655
|
args = ap.parse_args()
|
|
734
656
|
work_dir = Path(args.work_dir)
|
|
735
657
|
CONFIG["tts_provider"] = args.tts_provider
|
|
736
|
-
|
|
658
|
+
CONFIG["voice_ref"] = (
|
|
737
659
|
args.voice_ref if args.voice_ref is not None else os.environ.get("VOICE_REF", "").strip()
|
|
738
660
|
)
|
|
739
|
-
CONFIG["voice_ref"] = effective_voice_ref
|
|
740
|
-
for key in (
|
|
741
|
-
"voice_ref_b64",
|
|
742
|
-
"voice_ref_snapshot_path",
|
|
743
|
-
"voice_ref_snapshot_signature",
|
|
744
|
-
"voice_ref_fingerprint",
|
|
745
|
-
"voice_ref_source_signature",
|
|
746
|
-
"voice_ref_snapshot_locked",
|
|
747
|
-
):
|
|
748
|
-
CONFIG.pop(key, None)
|
|
749
661
|
if args.mimo_voice:
|
|
750
662
|
CONFIG["mimo_tts_voice"] = args.mimo_voice
|
|
751
|
-
if args.mimo_voice and CONFIG
|
|
663
|
+
if args.mimo_voice and CONFIG["voice_ref"]:
|
|
752
664
|
ap.error("--mimo-voice and --voice-ref are mutually exclusive")
|
|
753
|
-
if args.tts_provider == "fish-audio" and (args.mimo_voice or CONFIG
|
|
665
|
+
if args.tts_provider == "fish-audio" and (args.mimo_voice or CONFIG["voice_ref"]):
|
|
754
666
|
ap.error("--mimo-voice/--voice-ref are only supported by the MiMo TTS provider")
|
|
755
667
|
# Voice-reference normalization is intentionally lazy: a fully cached rerun should not
|
|
756
668
|
# invoke ffmpeg. _tts_mimo uses a process-wide lock so a fresh parallel run still converts
|
|
@@ -765,8 +677,7 @@ def main():
|
|
|
765
677
|
# silently override it; legacy direct-cut callers can still pass --narration explicitly.
|
|
766
678
|
narration_path = work_dir / "narration.json"
|
|
767
679
|
narration = json.loads(narration_path.read_text(encoding="utf-8"))
|
|
768
|
-
tts_segments, engine_used = synthesize_tts(narration, work_dir)
|
|
769
|
-
failures = getattr(synthesize_tts, "last_failures", [])
|
|
680
|
+
tts_segments, engine_used, failures = synthesize_tts(narration, work_dir)
|
|
770
681
|
meta = _build_tts_meta(tts_segments, engine_used, narration_path.name, failures)
|
|
771
682
|
(work_dir / "tts_meta.json").write_text(
|
|
772
683
|
json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|