@oh-story/dsh 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/lib/client.js +7 -3
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +212 -142
- package/lib/drama/skills/short-drama/SKILL.md +14 -2
- package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
- package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
- package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
- package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
- package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
- package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
- package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
- package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
- package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
- package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
- package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
- package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
- package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
- package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
- package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
- package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
- package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
- package/lib/index.js +71 -21
- package/lib/index.js.map +3 -3
- package/lib/oh-story/manifest.json +545 -310
- package/lib/oh-story/roles/chapter-extractor.md +11 -11
- package/lib/oh-story/roles/character-designer.md +1 -2
- package/lib/oh-story/roles/consistency-checker.md +11 -9
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +9 -61
- package/lib/oh-story/roles/story-explorer.md +20 -125
- package/lib/oh-story/roles/story-researcher.md +0 -1
- package/lib/oh-story/skills/story/SKILL.md +6 -6
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
- package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-import/SKILL.md +63 -579
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
- package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
- package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
- package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
- package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
- package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
- package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
- package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
- package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
- package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
- package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
- package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
- package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
- package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-review/SKILL.md +19 -273
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
- package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
- package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
- package/lib/oh-story/skills/story-review/references/solo.md +52 -0
- package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
- package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
- package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
- package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
- package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
- package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
- package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
- package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
- package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
- package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +2 -2
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +60 -1
- package/lib/video-recap/README.en.md +348 -137
- package/lib/video-recap/README.md +340 -136
- package/lib/video-recap/manifest.json +406 -251
- package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
- package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
- package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
- package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
- package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
- package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
- package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
- package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
- package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
- package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
- package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
- package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
- package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
- package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
- package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
- package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
- package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
- package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
- package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
- package/lib/video-recap/skills/video-script/SKILL.md +16 -13
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
- package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
- package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
- package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
- package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
- package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
- package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
- package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
- package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
- package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
- package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
- package/package.json +41 -41
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
- package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
- package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
- /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
- /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
- /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Fail-closed approved narration policy and current-metadata lifecycle helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
LEGACY_TEXT_POLICY = "legacy-auto-truncate-v1"
|
|
11
|
+
PRESERVE_APPROVED_TEXT_POLICY = "preserve-approved-text-v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApprovedTextPolicyError(RuntimeError):
|
|
15
|
+
def __init__(self, message, evidence):
|
|
16
|
+
self.evidence = evidence
|
|
17
|
+
super().__init__(
|
|
18
|
+
f"{message}: " + json.dumps(evidence, ensure_ascii=False, sort_keys=True)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ApprovedTextDurationError(ApprovedTextPolicyError):
|
|
23
|
+
"""Approved narration cannot fit its authored window without changing text."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, evidence):
|
|
26
|
+
super().__init__(
|
|
27
|
+
"批准旁白无法在不删改文本的前提下安全放入时间窗;请扩大窗口或由作者修订文本",
|
|
28
|
+
evidence,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def policy_name(preserve_approved_text):
|
|
33
|
+
if preserve_approved_text:
|
|
34
|
+
return PRESERVE_APPROVED_TEXT_POLICY
|
|
35
|
+
return LEGACY_TEXT_POLICY
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_required_texts(narration, clean_text, preserve_approved_text):
|
|
39
|
+
"""Reject authored segments whose provider-cleaned spoken text is empty."""
|
|
40
|
+
if not preserve_approved_text:
|
|
41
|
+
return
|
|
42
|
+
for index, segment in enumerate(narration):
|
|
43
|
+
spoken_text = clean_text(segment["narration"])
|
|
44
|
+
if spoken_text:
|
|
45
|
+
continue
|
|
46
|
+
evidence = {
|
|
47
|
+
"failure_kind": "approved_text_empty_after_cleanup",
|
|
48
|
+
"policy": PRESERVE_APPROVED_TEXT_POLICY,
|
|
49
|
+
"required": True,
|
|
50
|
+
"index": index,
|
|
51
|
+
"segment": index + 1,
|
|
52
|
+
"authored_text": segment["narration"],
|
|
53
|
+
"spoken_text": spoken_text,
|
|
54
|
+
"window_start": segment["start"],
|
|
55
|
+
"window_end": segment["end"],
|
|
56
|
+
}
|
|
57
|
+
raise ApprovedTextPolicyError(
|
|
58
|
+
"批准稿必需段经显式格式/舞台提示清理后没有可合成文本;请修订或删除该作者段",
|
|
59
|
+
evidence,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def enforce_duration(index, segment, authored_text, spoken_text, audio_duration,
|
|
64
|
+
available_duration, max_raw_duration, preserve_approved_text,
|
|
65
|
+
default_pause_ms):
|
|
66
|
+
if not preserve_approved_text or audio_duration <= max_raw_duration:
|
|
67
|
+
return
|
|
68
|
+
evidence = {
|
|
69
|
+
"failure_kind": "approved_text_duration_conflict",
|
|
70
|
+
"policy": PRESERVE_APPROVED_TEXT_POLICY,
|
|
71
|
+
"index": index,
|
|
72
|
+
"segment": index + 1,
|
|
73
|
+
"authored_text": authored_text,
|
|
74
|
+
"spoken_text": spoken_text,
|
|
75
|
+
"audio_duration": round(audio_duration, 6),
|
|
76
|
+
"window_start": segment["start"],
|
|
77
|
+
"window_end": segment["end"],
|
|
78
|
+
"window_duration": round(segment["end"] - segment["start"], 6),
|
|
79
|
+
"pause_after_ms": segment.get("pause_after_ms", default_pause_ms),
|
|
80
|
+
"available_duration": round(available_duration, 6),
|
|
81
|
+
"max_raw_duration": round(max_raw_duration, 6),
|
|
82
|
+
}
|
|
83
|
+
raise ApprovedTextDurationError(evidence)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def archive_current_meta(meta_path):
|
|
87
|
+
"""Atomically move a current meta file into timestamp-named history."""
|
|
88
|
+
path = Path(meta_path)
|
|
89
|
+
if not path.is_file():
|
|
90
|
+
return None
|
|
91
|
+
history_dir = path.parent / "tts_meta.history"
|
|
92
|
+
history_dir.mkdir(exist_ok=True)
|
|
93
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
|
|
94
|
+
archived = history_dir / f"{stamp}.json"
|
|
95
|
+
counter = 0
|
|
96
|
+
while archived.exists():
|
|
97
|
+
counter += 1
|
|
98
|
+
archived = history_dir / f"{stamp}-{counter}.json"
|
|
99
|
+
os.replace(path, archived)
|
|
100
|
+
return archived
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def write_json_atomically(path, payload):
|
|
104
|
+
"""Replace JSON only after a complete same-directory write and fsync."""
|
|
105
|
+
destination = Path(path)
|
|
106
|
+
temp_path = None
|
|
107
|
+
try:
|
|
108
|
+
with tempfile.NamedTemporaryFile(
|
|
109
|
+
mode="w",
|
|
110
|
+
encoding="utf-8",
|
|
111
|
+
dir=destination.parent,
|
|
112
|
+
prefix=f".{destination.stem}.",
|
|
113
|
+
suffix=".tmp",
|
|
114
|
+
delete=False,
|
|
115
|
+
) as handle:
|
|
116
|
+
temp_path = Path(handle.name)
|
|
117
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
118
|
+
handle.flush()
|
|
119
|
+
os.fsync(handle.fileno())
|
|
120
|
+
os.replace(temp_path, destination)
|
|
121
|
+
finally:
|
|
122
|
+
if temp_path is not None:
|
|
123
|
+
temp_path.unlink(missing_ok=True)
|
|
@@ -20,7 +20,6 @@ brittle and case-by-case. Two stages around an agent-authored pause:
|
|
|
20
20
|
"""
|
|
21
21
|
import argparse
|
|
22
22
|
import base64
|
|
23
|
-
import hashlib
|
|
24
23
|
import json
|
|
25
24
|
import re
|
|
26
25
|
import unicodedata
|
|
@@ -28,6 +27,7 @@ import wave
|
|
|
28
27
|
from pathlib import Path
|
|
29
28
|
|
|
30
29
|
from lib import (
|
|
30
|
+
file_identity,
|
|
31
31
|
CONFIG,
|
|
32
32
|
get_video_duration,
|
|
33
33
|
log,
|
|
@@ -44,7 +44,6 @@ ATEMPO_CAP = 2.0 # max compression before we trim instead (atempo>2 also sounds
|
|
|
44
44
|
DUB_FAST_SPEECH_CPS = 7.0
|
|
45
45
|
DUB_TRIM_RISK_CPS = 7.0
|
|
46
46
|
DUB_MIN_ASR_WINDOW_SECONDS = 0.5
|
|
47
|
-
DUB_TTS_CACHE_VERSION = 1
|
|
48
47
|
DUB_TTS_STYLE_PROMPT = "自然、清晰,保持原说话人的音色与节奏,语气平稳。"
|
|
49
48
|
|
|
50
49
|
DUB_SCHEMA_VERSION = 1
|
|
@@ -208,32 +207,32 @@ def lint_dub_script(script, duration, work_dir=None):
|
|
|
208
207
|
|
|
209
208
|
def build_dub_review(script, transcript, lint=None):
|
|
210
209
|
"""Script-level review scaffold: deterministic timing/naturalness signals for agent review."""
|
|
211
|
-
duration = float(transcript
|
|
210
|
+
duration = float(transcript["duration"])
|
|
212
211
|
lint = lint or lint_dub_script(script, duration)
|
|
213
212
|
lines = _normalize_dub_script(script) if isinstance(script, list) else []
|
|
214
213
|
edits = []
|
|
215
|
-
for issue in lint
|
|
214
|
+
for issue in lint["issues"]:
|
|
216
215
|
if issue["severity"] in {"error", "warning"}:
|
|
217
216
|
edits.append({
|
|
218
|
-
"line": issue
|
|
219
|
-
"start": issue.get("start"),
|
|
217
|
+
"line": issue["line"],
|
|
218
|
+
"start": issue.get("start"), # _issue only records start when the line had one
|
|
220
219
|
"issue": f"{issue['code']}: {issue['message']}",
|
|
221
220
|
"suggestion": "缩短译文、调整 start/end,或回到 transcript 核对原句边界。",
|
|
222
221
|
})
|
|
223
|
-
verdict = "FAIL" if lint
|
|
222
|
+
verdict = "FAIL" if lint["verdict"] == "FAIL" else ("REVISE" if edits else "PASS")
|
|
224
223
|
return {
|
|
225
224
|
"schema_version": DUB_SCHEMA_VERSION,
|
|
226
225
|
"verdict": verdict,
|
|
227
226
|
"checks": {
|
|
228
227
|
"faithful_to_source": "needs_agent_review",
|
|
229
|
-
"spoken_chinese": "REVISE" if any(i
|
|
228
|
+
"spoken_chinese": "REVISE" if any(i["code"] == "fast_speech" for i in lint["issues"]) else "PASS",
|
|
230
229
|
"speaker_tone": "needs_agent_review",
|
|
231
|
-
"timing_fit":
|
|
230
|
+
"timing_fit": verdict,
|
|
232
231
|
"platform_fit": "needs_agent_review",
|
|
233
232
|
},
|
|
234
233
|
"highest_return_edits": edits[:8],
|
|
235
234
|
"notes": "Deterministic script-level review; an agent/human should fill semantic fidelity and tone judgments against dub_transcript.json.",
|
|
236
|
-
"coverage": {"transcript_windows": len(transcript
|
|
235
|
+
"coverage": {"transcript_windows": len(transcript["windows"]), "script_lines": len(lines)},
|
|
237
236
|
}
|
|
238
237
|
|
|
239
238
|
|
|
@@ -244,12 +243,19 @@ def _write_json(path, payload):
|
|
|
244
243
|
|
|
245
244
|
# ── ffmpeg / wav helpers ─────────────────────────────────────────────
|
|
246
245
|
|
|
246
|
+
def _ffmpeg(args):
|
|
247
|
+
"""Run one ffmpeg command; a non-zero exit raises instead of leaving a silent gap."""
|
|
248
|
+
result = run_cmd(["ffmpeg", "-y", *args])
|
|
249
|
+
if result.returncode != 0:
|
|
250
|
+
raise RuntimeError(f"ffmpeg 失败 ({args[-1]}): {result.stderr.strip()[-2000:]}")
|
|
251
|
+
|
|
252
|
+
|
|
247
253
|
def _ffmpeg_extract_wav(video, out_wav, sr=16000):
|
|
248
|
-
|
|
254
|
+
_ffmpeg(["-i", str(video), "-vn", "-ar", str(sr), "-ac", "1", str(out_wav)])
|
|
249
255
|
|
|
250
256
|
|
|
251
257
|
def _cut_wav(src_wav, out_wav, start, dur, sr=16000):
|
|
252
|
-
|
|
258
|
+
_ffmpeg(["-i", str(src_wav), "-ss", str(start), "-t", str(dur),
|
|
253
259
|
"-ar", str(sr), "-ac", "1", str(out_wav)])
|
|
254
260
|
|
|
255
261
|
|
|
@@ -285,18 +291,14 @@ def _strip_reasoning_residue(text):
|
|
|
285
291
|
|
|
286
292
|
|
|
287
293
|
def _run_asr(wav_path, lang="en"):
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
raw = Path(wav_path).read_bytes()
|
|
295
|
-
if not raw:
|
|
296
|
-
return ""
|
|
297
|
-
b64 = base64.b64encode(raw).decode("ascii")
|
|
294
|
+
# The WAV was just cut by ffmpeg (which raised on failure); an unreadable file is a bug,
|
|
295
|
+
# not "no speech". A zero-frame window (audio shorter than the picture) is skipped.
|
|
296
|
+
with wave.open(str(wav_path), "rb") as source:
|
|
297
|
+
if source.getnframes() <= 0:
|
|
298
|
+
return ""
|
|
299
|
+
b64 = base64.b64encode(Path(wav_path).read_bytes()).decode("ascii")
|
|
298
300
|
payload = {
|
|
299
|
-
"model": CONFIG
|
|
301
|
+
"model": CONFIG["mimo_asr_model"],
|
|
300
302
|
"messages": [{"role": "user", "content": [
|
|
301
303
|
{"type": "input_audio", "input_audio": {"data": f"data:{ASR_MIME};base64,{b64}"}},
|
|
302
304
|
]}],
|
|
@@ -305,8 +307,8 @@ def _run_asr(wav_path, lang="en"):
|
|
|
305
307
|
resp = mimo_asr_api_call(payload)
|
|
306
308
|
try:
|
|
307
309
|
text = str(resp["choices"][0]["message"]["content"] or "").strip()
|
|
308
|
-
except (KeyError, IndexError, TypeError):
|
|
309
|
-
|
|
310
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
311
|
+
raise RuntimeError("MiMo-ASR 响应缺少 choices[0].message.content") from exc
|
|
310
312
|
return re.sub(r"<[^>]{1,20}>", "", _strip_reasoning_residue(text)).strip() # strip reasoning + ASR markers like "<chinese>"
|
|
311
313
|
|
|
312
314
|
|
|
@@ -346,16 +348,13 @@ def _clone_tts(text, ref_b64, out_wav):
|
|
|
346
348
|
Path(out_wav).write_bytes(base64.b64decode(data))
|
|
347
349
|
|
|
348
350
|
|
|
349
|
-
def
|
|
350
|
-
|
|
351
|
-
"cache_version": DUB_TTS_CACHE_VERSION,
|
|
351
|
+
def _clone_cache_inputs(text, ref_identity):
|
|
352
|
+
return {
|
|
352
353
|
"model": CLONE_MODEL,
|
|
353
354
|
"style_prompt": DUB_TTS_STYLE_PROMPT,
|
|
354
355
|
"text": str(text),
|
|
355
|
-
"
|
|
356
|
+
"reference": ref_identity,
|
|
356
357
|
}
|
|
357
|
-
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
358
|
-
return hashlib.sha256(encoded).hexdigest()
|
|
359
358
|
|
|
360
359
|
|
|
361
360
|
def _clone_cache_meta_path(raw_wav):
|
|
@@ -371,30 +370,27 @@ def _usable_clone_wav(path):
|
|
|
371
370
|
return sample_rate > 0 and channels > 0 and bool(frames)
|
|
372
371
|
|
|
373
372
|
|
|
374
|
-
def _write_clone_cache_meta(raw_wav, text,
|
|
375
|
-
meta = {
|
|
376
|
-
"schema_version": DUB_TTS_CACHE_VERSION,
|
|
377
|
-
"fingerprint": _clone_cache_fingerprint(text, ref_bytes),
|
|
378
|
-
"model": CLONE_MODEL,
|
|
379
|
-
}
|
|
373
|
+
def _write_clone_cache_meta(raw_wav, text, ref_identity):
|
|
374
|
+
meta = {"inputs": _clone_cache_inputs(text, ref_identity)}
|
|
380
375
|
return _write_json(_clone_cache_meta_path(raw_wav), meta)
|
|
381
376
|
|
|
382
377
|
|
|
383
|
-
def _ensure_clone_tts(text, ref_b64,
|
|
378
|
+
def _ensure_clone_tts(text, ref_b64, ref_identity, raw_wav):
|
|
384
379
|
raw_wav = Path(raw_wav)
|
|
385
|
-
expected =
|
|
380
|
+
expected = _clone_cache_inputs(text, ref_identity)
|
|
386
381
|
meta_path = _clone_cache_meta_path(raw_wav)
|
|
387
|
-
|
|
382
|
+
# Missing sidecar = never synthesized; a sidecar this skill wrote but cannot parse is a
|
|
383
|
+
# bug, not a reason to silently pay for re-synthesis.
|
|
384
|
+
if meta_path.exists():
|
|
388
385
|
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
389
|
-
|
|
390
|
-
meta
|
|
391
|
-
|
|
392
|
-
return True
|
|
386
|
+
# A meta from the content-hash schema has no "inputs" and is simply a miss.
|
|
387
|
+
if meta.get("inputs") == expected and _usable_clone_wav(raw_wav):
|
|
388
|
+
return True
|
|
393
389
|
|
|
394
390
|
_clone_tts(text, ref_b64, raw_wav)
|
|
395
391
|
if not _usable_clone_wav(raw_wav):
|
|
396
392
|
raise RuntimeError(f"MiMo voiceclone returned an invalid WAV: {raw_wav}")
|
|
397
|
-
_write_clone_cache_meta(raw_wav, text,
|
|
393
|
+
_write_clone_cache_meta(raw_wav, text, ref_identity)
|
|
398
394
|
return False
|
|
399
395
|
|
|
400
396
|
|
|
@@ -403,17 +399,15 @@ def _time_fit(raw_wav, fitted_wav, room_seconds):
|
|
|
403
399
|
the next line). Never globally speed up; a short dub keeps the natural pause. Returns the
|
|
404
400
|
fitted duration."""
|
|
405
401
|
dur = get_video_duration(raw_wav)
|
|
406
|
-
if dur <= 0:
|
|
407
|
-
return 0.0
|
|
408
402
|
if dur <= room_seconds + 0.05:
|
|
409
|
-
|
|
403
|
+
_ffmpeg(["-i", str(raw_wav), "-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
|
|
410
404
|
return dur
|
|
411
405
|
factor = dur / room_seconds
|
|
412
406
|
if factor <= ATEMPO_CAP:
|
|
413
|
-
|
|
407
|
+
_ffmpeg(["-i", str(raw_wav), "-filter:a", _atempo_chain(factor),
|
|
414
408
|
"-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
|
|
415
409
|
return get_video_duration(fitted_wav)
|
|
416
|
-
|
|
410
|
+
_ffmpeg(["-i", str(raw_wav),
|
|
417
411
|
"-filter:a", f"{_atempo_chain(ATEMPO_CAP)},atrim=0:{room_seconds:.3f},"
|
|
418
412
|
f"afade=t=out:st={max(0, room_seconds - 0.15):.3f}:d=0.15",
|
|
419
413
|
"-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
|
|
@@ -423,12 +417,10 @@ def _time_fit(raw_wav, fitted_wav, room_seconds):
|
|
|
423
417
|
def _build_dub_track(lines, duration, out_wav):
|
|
424
418
|
canvas = bytearray(int(duration * CLONE_SR) * 2) # 16-bit mono silence
|
|
425
419
|
for ln in lines:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
continue
|
|
429
|
-
sr, ch, frames = _wav_frames(fw)
|
|
420
|
+
# _time_fit always writes fitted_wav at CLONE_SR mono (and raised if ffmpeg failed).
|
|
421
|
+
sr, ch, frames = _wav_frames(ln["fitted_wav"])
|
|
430
422
|
if sr != CLONE_SR or ch != 1:
|
|
431
|
-
|
|
423
|
+
raise RuntimeError(f"fitted WAV {ln['fitted_wav']} is {sr}Hz/{ch}ch, expected {CLONE_SR}Hz mono")
|
|
432
424
|
off = int(ln["start"] * CLONE_SR) * 2
|
|
433
425
|
end = min(off + len(frames), len(canvas))
|
|
434
426
|
canvas[off:end] = frames[: end - off]
|
|
@@ -440,7 +432,7 @@ def _build_dub_track(lines, duration, out_wav):
|
|
|
440
432
|
|
|
441
433
|
|
|
442
434
|
def _mux(video, dub_wav, out_video):
|
|
443
|
-
|
|
435
|
+
_ffmpeg(["-i", str(video), "-i", str(dub_wav),
|
|
444
436
|
"-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy",
|
|
445
437
|
"-af", "loudnorm=I=-16:TP=-1.5:LRA=11", "-c:a", "aac", "-b:a", "192k",
|
|
446
438
|
"-ar", str(DUB_DELIVERY_SR),
|
|
@@ -529,8 +521,8 @@ def stage_render(video, work, ref_start, ref_dur):
|
|
|
529
521
|
if not ref_wav.exists():
|
|
530
522
|
rs, rd = _ref_window(duration, ref_start, ref_dur)
|
|
531
523
|
_cut_wav(work / "dub_source.wav", ref_wav, rs, rd)
|
|
532
|
-
|
|
533
|
-
ref_b64 = base64.b64encode(
|
|
524
|
+
ref_identity = file_identity(ref_wav)
|
|
525
|
+
ref_b64 = base64.b64encode(ref_wav.read_bytes()).decode("ascii")
|
|
534
526
|
|
|
535
527
|
tts_dir = work / "dub_tts"
|
|
536
528
|
tts_dir.mkdir(exist_ok=True)
|
|
@@ -542,7 +534,7 @@ def stage_render(video, work, ref_start, ref_dur):
|
|
|
542
534
|
room = max(0.4, min(slot_end, nxt) - ln["start"])
|
|
543
535
|
raw = tts_dir / f"line_{i:03d}_raw.wav"
|
|
544
536
|
fitted = tts_dir / f"line_{i:03d}.wav"
|
|
545
|
-
cache_hit = _ensure_clone_tts(ln["zh"], ref_b64,
|
|
537
|
+
cache_hit = _ensure_clone_tts(ln["zh"], ref_b64, ref_identity, raw)
|
|
546
538
|
ln["tts_cache"] = "hit" if cache_hit else "miss"
|
|
547
539
|
ln["fitted_wav"] = str(fitted)
|
|
548
540
|
ln["fitted_dur"] = round(_time_fit(raw, fitted, room), 2)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Self-contained config + utilities for this skill (no cross-skill imports).
|
|
2
2
|
Merged from the shared core; reads the same env vars as the rest of the bundle."""
|
|
3
3
|
import json
|
|
4
|
-
import hashlib
|
|
5
4
|
import math
|
|
6
5
|
import os
|
|
7
6
|
import re
|
|
@@ -116,7 +115,7 @@ CONFIG = {
|
|
|
116
115
|
"mimo_api_key": _mimo_api_key,
|
|
117
116
|
"mimo_tts_api_url": normalize_api_url(_raw_mimo_tts_api_url),
|
|
118
117
|
"mimo_tts_api_key": _mimo_tts_api_key,
|
|
119
|
-
"
|
|
118
|
+
"mimo_tts_env_var": "MIMO_TTS_API_KEY" if os.environ.get("MIMO_TTS_API_KEY") else "MIMO_API_KEY",
|
|
120
119
|
"mimo_asr_model": os.environ.get("MIMO_ASR_MODEL", DEFAULT_MIMO_ASR_MODEL),
|
|
121
120
|
"mimo_tts_model": os.environ.get("MIMO_TTS_MODEL", DEFAULT_MIMO_TTS_MODEL),
|
|
122
121
|
"mimo_tts_voice": os.environ.get("MIMO_TTS_VOICE", "冰糖"),
|
|
@@ -190,45 +189,10 @@ def get_video_duration(video_path):
|
|
|
190
189
|
return float(result.stdout.strip())
|
|
191
190
|
|
|
192
191
|
|
|
193
|
-
def
|
|
194
|
-
"""
|
|
195
|
-
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
196
|
-
return hashlib.md5(encoded.encode("utf-8")).hexdigest()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
_FILE_FINGERPRINT_MEMO = {}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _file_identity(path):
|
|
203
|
-
"""(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
|
|
192
|
+
def file_identity(path):
|
|
193
|
+
"""{size, mtime_ns} — the cache identity of an input file (no content read)."""
|
|
204
194
|
st = os.stat(os.fspath(path))
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def file_fingerprint(path, chunk_size=1024 * 1024):
|
|
209
|
-
"""Return a full-content fingerprint for cache-correct identity checks.
|
|
210
|
-
|
|
211
|
-
The digest covers CONTENT only — never the path or mtime — so a copied video or
|
|
212
|
-
artifact is still recognised as the same asset, while any byte change invalidates
|
|
213
|
-
the cache even if timestamps, size, head, or tail bytes are misleading.
|
|
214
|
-
|
|
215
|
-
Identity metadata is used ONLY to memoize within a single process. One understanding
|
|
216
|
-
run fingerprints the same source video 8-10 times and the whole extracted frame set
|
|
217
|
-
2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
|
|
218
|
-
any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
|
|
219
|
-
re-hashed, so the memo can never serve a stale digest.
|
|
220
|
-
"""
|
|
221
|
-
key = _file_identity(path)
|
|
222
|
-
memoized = _FILE_FINGERPRINT_MEMO.get(key)
|
|
223
|
-
if memoized is not None:
|
|
224
|
-
return memoized
|
|
225
|
-
h = hashlib.sha256()
|
|
226
|
-
with open(os.fspath(path), "rb") as f:
|
|
227
|
-
for chunk in iter(lambda: f.read(chunk_size), b""):
|
|
228
|
-
h.update(chunk)
|
|
229
|
-
digest = h.hexdigest()
|
|
230
|
-
_FILE_FINGERPRINT_MEMO[key] = digest
|
|
231
|
-
return digest
|
|
195
|
+
return {"size": st.st_size, "mtime_ns": st.st_mtime_ns}
|
|
232
196
|
|
|
233
197
|
|
|
234
198
|
def _retry_after_seconds(value, fallback):
|
|
@@ -288,7 +252,7 @@ def mimo_tts_api_call(payload):
|
|
|
288
252
|
max_retries=10,
|
|
289
253
|
api_url=CONFIG["mimo_tts_api_url"],
|
|
290
254
|
api_key=CONFIG["mimo_tts_api_key"],
|
|
291
|
-
|
|
255
|
+
api_env_var=CONFIG["mimo_tts_env_var"],
|
|
292
256
|
)
|
|
293
257
|
|
|
294
258
|
|
|
@@ -299,11 +263,11 @@ def mimo_asr_api_call(payload):
|
|
|
299
263
|
max_retries=10,
|
|
300
264
|
api_url=CONFIG["mimo_api_url"],
|
|
301
265
|
api_key=CONFIG["mimo_api_key"],
|
|
302
|
-
|
|
266
|
+
api_env_var="MIMO_API_KEY",
|
|
303
267
|
)
|
|
304
268
|
|
|
305
269
|
|
|
306
|
-
def api_call(payload, *, api_url, api_key,
|
|
270
|
+
def api_call(payload, *, api_url, api_key, api_env_var, max_retries=8):
|
|
307
271
|
"""调用 OpenAI-compatible API,带重试。
|
|
308
272
|
|
|
309
273
|
集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
|
|
@@ -330,7 +294,7 @@ def api_call(payload, *, api_url, api_key, api_key_source, max_retries=8):
|
|
|
330
294
|
wait = _retry_after_seconds(retry_after, max(wait, 10))
|
|
331
295
|
log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
|
|
332
296
|
elif e.code == 401:
|
|
333
|
-
raise RuntimeError(f"API 认证失败 (401)。请检查 {
|
|
297
|
+
raise RuntimeError(f"API 认证失败 (401)。请检查 {api_env_var} 和 API URL 是否匹配。")
|
|
334
298
|
elif e.code == 403:
|
|
335
299
|
hint = "API 访问被拒绝 (403)。"
|
|
336
300
|
if "1010" in body or "cloudflare" in body.lower():
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TTS provider transports (fish-audio, index-tts) for the video-voiceover skill."""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Self-hosted index-tts JSON-to-WAV transport with no redirect forwarding."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
import wave
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MAX_WAV_BYTES = 64 * 1024 * 1024
|
|
16
|
+
MAX_ERROR_BYTES = 512
|
|
17
|
+
SPEED_POLICY = "provider-default-no-rate-control"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _SafeResponseError(RuntimeError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
25
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _open_without_redirects(request, timeout):
|
|
30
|
+
return urllib.request.build_opener(_NoRedirectHandler()).open(request, timeout=timeout)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_index_tts_config(endpoint, voice):
|
|
34
|
+
"""Validate explicit private settings without returning a credential-bearing URL."""
|
|
35
|
+
endpoint = str(endpoint or "")
|
|
36
|
+
voice = str(voice or "").strip()
|
|
37
|
+
if not endpoint:
|
|
38
|
+
raise ValueError("INDEX_TTS_ENDPOINT 必须显式配置")
|
|
39
|
+
if any(ord(char) < 32 or ord(char) == 127 for char in endpoint):
|
|
40
|
+
raise ValueError("INDEX_TTS_ENDPOINT 配置无效或包含禁止字符")
|
|
41
|
+
try:
|
|
42
|
+
parsed = urllib.parse.urlsplit(endpoint)
|
|
43
|
+
port = parsed.port
|
|
44
|
+
except ValueError:
|
|
45
|
+
raise ValueError("INDEX_TTS_ENDPOINT 配置无效") from None
|
|
46
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
47
|
+
raise ValueError("INDEX_TTS_ENDPOINT 必须是带主机名的 http/https URL")
|
|
48
|
+
if "@" in parsed.netloc or "?" in endpoint or "#" in endpoint:
|
|
49
|
+
raise ValueError("INDEX_TTS_ENDPOINT 禁止 userinfo、query 或 fragment 携带凭证")
|
|
50
|
+
if port is not None and not 1 <= port <= 65535:
|
|
51
|
+
raise ValueError("INDEX_TTS_ENDPOINT 配置无效")
|
|
52
|
+
if not voice:
|
|
53
|
+
raise ValueError("INDEX_TTS_VOICE 必须显式配置")
|
|
54
|
+
return endpoint, voice
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_private_config(config, environ):
|
|
58
|
+
"""Read the private index-tts settings; validate them once, only when that provider is selected."""
|
|
59
|
+
endpoint = environ.get("INDEX_TTS_ENDPOINT", "").strip()
|
|
60
|
+
voice = environ.get("INDEX_TTS_VOICE", "").strip()
|
|
61
|
+
if config["tts_provider"] == "index-tts":
|
|
62
|
+
endpoint, voice = validate_index_tts_config(endpoint, voice)
|
|
63
|
+
config["index_tts_endpoint"] = endpoint
|
|
64
|
+
config["index_tts_voice"] = voice
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cache_settings(config):
|
|
68
|
+
"""Non-secret settings that decide reuse; the endpoint is never written to disk."""
|
|
69
|
+
return {
|
|
70
|
+
"index_tts_voice": config["index_tts_voice"],
|
|
71
|
+
"index_tts_speed_policy": SPEED_POLICY,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def default_controls(segment):
|
|
76
|
+
unsupported = []
|
|
77
|
+
for key, default in (("emotion", None), ("style", None), ("rate", "+0%"), ("pitch", "+0Hz")):
|
|
78
|
+
if segment.get(key) not in (None, "", default):
|
|
79
|
+
unsupported.append(key)
|
|
80
|
+
if unsupported:
|
|
81
|
+
raise RuntimeError(f"已配置的端点不接受段级控制字段: {', '.join(unsupported)}")
|
|
82
|
+
return "+0%", "+0Hz"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def synthesize_configured(text, output_path, config):
|
|
86
|
+
return synthesize_index_tts(
|
|
87
|
+
text, output_path, endpoint=config["index_tts_endpoint"],
|
|
88
|
+
voice=config["index_tts_voice"], timeout=config["tts_timeout"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def valid_cached_receipt(cache_data, config):
|
|
93
|
+
"""A cached index-tts segment must carry the receipt of a request for the current voice."""
|
|
94
|
+
receipt = cache_data.get("provider_receipt")
|
|
95
|
+
return isinstance(receipt, dict) and receipt == {
|
|
96
|
+
"provider": "index-tts",
|
|
97
|
+
"requested_voice": config["index_tts_voice"],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _discard_http_error_body(error):
|
|
102
|
+
"""Drain a bounded slice and close; the body is never surfaced (it may echo private URLs)."""
|
|
103
|
+
with contextlib.suppress(Exception):
|
|
104
|
+
try:
|
|
105
|
+
error.read(MAX_ERROR_BYTES)
|
|
106
|
+
finally:
|
|
107
|
+
error.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _validate_wav(audio):
|
|
111
|
+
if len(audio) <= 44 or audio[:4] != b"RIFF" or audio[8:12] != b"WAVE":
|
|
112
|
+
raise RuntimeError("IndexTTS 返回的内容不是有效 WAV")
|
|
113
|
+
if int.from_bytes(audio[4:8], "little") + 8 > len(audio):
|
|
114
|
+
raise RuntimeError("IndexTTS 返回的 RIFF 声明长度超过响应,音频被截断")
|
|
115
|
+
try:
|
|
116
|
+
with wave.open(io.BytesIO(audio), "rb") as wav:
|
|
117
|
+
channels = wav.getnchannels()
|
|
118
|
+
sample_width = wav.getsampwidth()
|
|
119
|
+
sample_rate = wav.getframerate()
|
|
120
|
+
frames = wav.getnframes()
|
|
121
|
+
decoded = wav.readframes(frames)
|
|
122
|
+
except (EOFError, wave.Error) as exc:
|
|
123
|
+
raise RuntimeError("IndexTTS 返回的 WAV 结构损坏或不受支持") from exc
|
|
124
|
+
if not (1 <= channels <= 8 and 1 <= sample_width <= 4 and 8000 <= sample_rate <= 192000):
|
|
125
|
+
raise RuntimeError("IndexTTS 返回的 WAV 音频参数越界")
|
|
126
|
+
if frames <= 0:
|
|
127
|
+
raise RuntimeError("IndexTTS 返回的 WAV 没有音频帧")
|
|
128
|
+
if len(decoded) != frames * channels * sample_width:
|
|
129
|
+
raise RuntimeError("IndexTTS 返回的 WAV 音频数据被截断")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def synthesize_index_tts(text, output_path, *, endpoint, voice, timeout):
|
|
133
|
+
"""POST the exact {voice,text} contract and atomically persist an authentic WAV.
|
|
134
|
+
|
|
135
|
+
endpoint/voice were validated by load_private_config; timeout by lib's env_int."""
|
|
136
|
+
body = json.dumps({"voice": voice, "text": text}, ensure_ascii=False).encode("utf-8")
|
|
137
|
+
request = urllib.request.Request(
|
|
138
|
+
endpoint,
|
|
139
|
+
data=body,
|
|
140
|
+
headers={
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"Accept": "audio/wav",
|
|
143
|
+
"User-Agent": "video-recap/1.0",
|
|
144
|
+
},
|
|
145
|
+
method="POST",
|
|
146
|
+
)
|
|
147
|
+
try:
|
|
148
|
+
with _open_without_redirects(request, timeout) as response:
|
|
149
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
|
150
|
+
content_length = response.headers.get("Content-Length")
|
|
151
|
+
if content_length and int(content_length) > MAX_WAV_BYTES:
|
|
152
|
+
raise _SafeResponseError("IndexTTS 返回 WAV 过大,已拒绝读取")
|
|
153
|
+
audio = response.read(MAX_WAV_BYTES + 1)
|
|
154
|
+
except urllib.error.HTTPError as exc:
|
|
155
|
+
_discard_http_error_body(exc)
|
|
156
|
+
if 300 <= exc.code < 400:
|
|
157
|
+
raise RuntimeError("IndexTTS 禁止 HTTP 重定向,未向其他主机转发批准文本") from None
|
|
158
|
+
raise RuntimeError(f"IndexTTS 请求失败 (HTTP {exc.code}),远端返回错误状态") from None
|
|
159
|
+
except urllib.error.URLError as exc:
|
|
160
|
+
kind = "超时" if isinstance(exc.reason, (TimeoutError, socket.timeout)) else "网络"
|
|
161
|
+
raise RuntimeError(f"IndexTTS {kind}错误,未返回音频") from None
|
|
162
|
+
except (TimeoutError, socket.timeout):
|
|
163
|
+
raise RuntimeError("IndexTTS 超时错误,未返回音频") from None
|
|
164
|
+
except ValueError:
|
|
165
|
+
raise RuntimeError("IndexTTS 返回了无效的 Content-Length") from None
|
|
166
|
+
except _SafeResponseError as exc:
|
|
167
|
+
raise RuntimeError(str(exc)) from None
|
|
168
|
+
except Exception:
|
|
169
|
+
raise RuntimeError("IndexTTS 响应读取或关闭失败,未保存音频") from None
|
|
170
|
+
|
|
171
|
+
if len(audio) > MAX_WAV_BYTES:
|
|
172
|
+
raise RuntimeError("IndexTTS 返回 WAV 过大,已拒绝保存")
|
|
173
|
+
if "json" in content_type:
|
|
174
|
+
raise RuntimeError("IndexTTS 返回了 JSON 错误响应而不是音频;正文已隐藏")
|
|
175
|
+
_validate_wav(audio)
|
|
176
|
+
|
|
177
|
+
output = Path(output_path)
|
|
178
|
+
partial = Path(str(output) + ".part")
|
|
179
|
+
try:
|
|
180
|
+
partial.write_bytes(audio)
|
|
181
|
+
os.replace(partial, output)
|
|
182
|
+
finally:
|
|
183
|
+
partial.unlink(missing_ok=True)
|
|
184
|
+
return {"provider": "index-tts", "requested_voice": voice}
|