@oh-story/dsh 0.1.3 → 0.1.5
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 +58 -12
- package/lib/client.js +2970 -537
- package/lib/client.js.map +4 -4
- package/lib/drama/manifest.json +218 -178
- package/lib/drama/skills/short-drama/SKILL.md +110 -117
- package/lib/drama/skills/short-drama/references/audience-reveal.md +14 -67
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +26 -77
- package/lib/drama/skills/short-drama/references/creator-documents.md +204 -0
- package/lib/drama/skills/short-drama/references/creator-workflow.md +73 -63
- package/lib/drama/skills/short-drama/references/knowhow-index.md +5 -0
- package/lib/drama/skills/short-drama/references/look-development.md +2 -3
- package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +14 -48
- package/lib/drama/skills/short-drama/references/production-form-profiles.md +1 -1
- package/lib/drama/skills/short-drama/references/reference-roles.md +32 -135
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +16 -19
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
- package/lib/drama/skills/short-drama-assets/SKILL.md +59 -170
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +18 -25
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +13 -15
- package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +11 -14
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
- package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +6 -12
- package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +8 -11
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +21 -2
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +12 -105
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +12 -32
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +45 -155
- package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +12 -13
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +12 -13
- package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +39 -51
- package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +3 -4
- package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +15 -14
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +11 -15
- package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +4 -7
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +23 -23
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +8 -65
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
- package/lib/drama/skills/short-drama-produce/SKILL.md +67 -15
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
- package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
- package/lib/drama/skills/short-drama-review/SKILL.md +61 -123
- package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +10 -15
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +15 -16
- package/lib/drama/skills/short-drama-review/references/project-calibration.md +7 -9
- package/lib/drama/skills/short-drama-review/references/review-method.md +8 -9
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +9 -13
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +104 -4
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +27 -19
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +4 -63
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +116 -238
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +9 -34
- package/lib/drama/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
- package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +5 -10
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +12 -7
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
- package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +27 -62
- package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +16 -25
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +21 -23
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +8 -10
- package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +9 -13
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +16 -71
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +123 -230
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
- package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +10 -8
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +24 -6
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +2 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +35 -27
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -11
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +13 -10
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +34 -63
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +17 -73
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +74 -268
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +7 -0
- package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +17 -43
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +15 -15
- package/lib/drama/skills/short-drama-write/references/script-craft.md +208 -21
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +11 -70
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
- package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
- package/lib/index.js +971 -46
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/LICENSE +21 -0
- package/lib/novel-to-game/README.md +180 -0
- package/lib/novel-to-game/README_ZH.md +178 -0
- package/lib/novel-to-game/VERSION +1 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
- package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
- package/lib/novel-to-game/manifest.json +508 -0
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
- package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
- package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
- package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
- package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
- package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
- package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
- package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
- package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
- package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
- package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
- package/lib/oh-story/manifest.json +419 -309
- package/lib/oh-story/roles/character-designer.md +4 -4
- package/lib/oh-story/roles/consistency-checker.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +41 -42
- package/lib/oh-story/roles/story-architect.md +19 -23
- package/lib/oh-story/roles/story-explorer.md +8 -5
- package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +16 -9
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-import/SKILL.md +21 -8
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
- package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
- package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
- package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/story-long-write/references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-long-write/references/{reversal-toolkit.md → long-reversal.md} +18 -32
- package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +5 -4
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +17 -11
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-review/SKILL.md +26 -17
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +85 -41
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
- 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/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
- package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/story-setup/references/agent-references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
- package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-setup/references/agent-references/long-reversal.md} +18 -32
- package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
- package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
- package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → short-reversal.md} +29 -23
- package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
- 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/writing-craft.md +55 -55
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
- package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
- package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
- package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
- package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
- package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
- package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
- package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -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 +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
- 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 +6 -6
- 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 +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
- 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 +32 -44
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
- package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
- package/lib/types/client/drama-production-view.d.ts +34 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -0
- package/lib/types/client/drama-production.d.ts +92 -0
- package/lib/types/client/drama-production.d.ts.map +1 -0
- package/lib/types/client/file-activity.d.ts +9 -2
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-intents.d.ts +9 -0
- package/lib/types/client/production-intents.d.ts.map +1 -0
- package/lib/types/client/production-prompts.d.ts +9 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -0
- package/lib/types/client/production-runtime.d.ts +58 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -0
- package/lib/types/client/video-studio.d.ts +41 -0
- package/lib/types/client/video-studio.d.ts.map +1 -0
- package/lib/types/client/workbench-ui.d.ts +5 -0
- package/lib/types/client/workbench-ui.d.ts.map +1 -0
- package/lib/types/game-verification.d.ts +16 -0
- package/lib/types/game-verification.d.ts.map +1 -0
- package/lib/types/index.d.ts +5 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/production-intent.d.ts +21 -0
- package/lib/types/production-intent.d.ts.map +1 -0
- package/lib/types/production-tool.d.ts +5 -0
- package/lib/types/production-tool.d.ts.map +1 -0
- package/lib/types/reference-tool.d.ts +6 -0
- package/lib/types/reference-tool.d.ts.map +1 -0
- package/lib/types/role-provider.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts +3 -1
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts +7 -0
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts +42 -0
- package/lib/types/video-project.d.ts.map +1 -0
- package/lib/types/workspace-request-trust.d.ts +6 -0
- package/lib/types/workspace-request-trust.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts +20 -0
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +282 -0
- package/lib/video-recap/LICENSE +21 -0
- package/lib/video-recap/README.en.md +257 -0
- package/lib/video-recap/README.md +255 -0
- package/lib/video-recap/manifest.json +684 -0
- package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
- package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
- package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
- package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
- package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
- package/lib/video-recap/skills/video-script/SKILL.md +248 -0
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
- package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
- package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
- package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
- package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
- package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
- package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
- package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
- package/package.json +57 -41
- package/lib/drama/skills/short-drama/references/lifecycle-commands.md +0 -130
- package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
- package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
- package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
- package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
- package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
- package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
- package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
- package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
- /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
- /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Fish Audio TTS transport for the video-voiceover skill.
|
|
2
|
+
|
|
3
|
+
The provider returns audio bytes rather than JSON. Keep the request, response validation,
|
|
4
|
+
and secret-safe error handling isolated here so voiceover.py only selects an engine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from lib import (
|
|
14
|
+
CONFIG,
|
|
15
|
+
DEFAULT_FISH_TTS_API_URL,
|
|
16
|
+
DEFAULT_FISH_TTS_MODEL,
|
|
17
|
+
_sanitize_api_error,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fish_payload(text, rate="+0%"):
|
|
22
|
+
try:
|
|
23
|
+
speed = 1.0 + float(str(rate).rstrip("%")) / 100.0
|
|
24
|
+
except (TypeError, ValueError):
|
|
25
|
+
speed = 1.0
|
|
26
|
+
payload = {
|
|
27
|
+
"text": text,
|
|
28
|
+
"format": "wav",
|
|
29
|
+
"normalize": True,
|
|
30
|
+
"prosody": {
|
|
31
|
+
"speed": max(0.5, min(2.0, speed)),
|
|
32
|
+
"volume": 0,
|
|
33
|
+
"normalize_loudness": True,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
reference_id = str(CONFIG.get("fish_tts_reference_id") or "").strip()
|
|
37
|
+
if reference_id:
|
|
38
|
+
payload["reference_id"] = reference_id
|
|
39
|
+
return payload
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def synthesize_fish_audio(text, output_path, rate="+0%", **_ignored):
|
|
43
|
+
"""Synthesize one narration block and atomically write the WAV response."""
|
|
44
|
+
api_key = str(CONFIG.get("fish_api_key") or "").strip()
|
|
45
|
+
if not api_key:
|
|
46
|
+
raise RuntimeError("请设置 FISH_API_KEY 用于 Fish Audio TTS")
|
|
47
|
+
|
|
48
|
+
endpoint = str(CONFIG.get("fish_tts_api_url") or DEFAULT_FISH_TTS_API_URL)
|
|
49
|
+
model = str(CONFIG.get("fish_tts_model") or DEFAULT_FISH_TTS_MODEL)
|
|
50
|
+
body = json.dumps(_fish_payload(text, rate), ensure_ascii=False).encode("utf-8")
|
|
51
|
+
request = urllib.request.Request(
|
|
52
|
+
endpoint,
|
|
53
|
+
data=body,
|
|
54
|
+
headers={
|
|
55
|
+
"Authorization": f"Bearer {api_key}",
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Accept": "audio/wav",
|
|
58
|
+
"model": model,
|
|
59
|
+
"User-Agent": "video-recap/1.0",
|
|
60
|
+
},
|
|
61
|
+
method="POST",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
timeout = max(1, int(CONFIG.get("tts_timeout", 300) or 300))
|
|
65
|
+
try:
|
|
66
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
67
|
+
audio = response.read()
|
|
68
|
+
content_type = str(response.headers.get("Content-Type") or "").lower()
|
|
69
|
+
except urllib.error.HTTPError as exc:
|
|
70
|
+
detail = _sanitize_api_error(
|
|
71
|
+
exc.read().decode("utf-8", errors="replace"),
|
|
72
|
+
extra_secrets=(api_key,),
|
|
73
|
+
)
|
|
74
|
+
if exc.code == 401:
|
|
75
|
+
raise RuntimeError("Fish Audio 认证失败 (401),请检查 FISH_API_KEY") from exc
|
|
76
|
+
if exc.code == 402:
|
|
77
|
+
raise RuntimeError("Fish Audio 请求被拒绝 (402),请检查模型额度或计费状态") from exc
|
|
78
|
+
raise RuntimeError(f"Fish Audio TTS 请求失败 (HTTP {exc.code}): {detail}") from exc
|
|
79
|
+
except urllib.error.URLError as exc:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
"Fish Audio TTS 网络错误: "
|
|
82
|
+
f"{_sanitize_api_error(exc.reason, extra_secrets=(api_key,))}"
|
|
83
|
+
) from exc
|
|
84
|
+
|
|
85
|
+
if "json" in content_type:
|
|
86
|
+
detail = _sanitize_api_error(
|
|
87
|
+
audio.decode("utf-8", errors="replace"),
|
|
88
|
+
extra_secrets=(api_key,),
|
|
89
|
+
)
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
f"Fish Audio TTS 返回了 JSON 而不是音频: {detail}"
|
|
92
|
+
)
|
|
93
|
+
if (
|
|
94
|
+
len(audio) <= 44
|
|
95
|
+
or audio[:4] != b"RIFF"
|
|
96
|
+
or audio[8:12] != b"WAVE"
|
|
97
|
+
):
|
|
98
|
+
raise RuntimeError("Fish Audio TTS 返回的内容不是有效 WAV")
|
|
99
|
+
|
|
100
|
+
output = Path(output_path)
|
|
101
|
+
partial = Path(str(output) + ".part")
|
|
102
|
+
try:
|
|
103
|
+
partial.write_bytes(audio)
|
|
104
|
+
os.replace(partial, output)
|
|
105
|
+
finally:
|
|
106
|
+
try:
|
|
107
|
+
partial.unlink(missing_ok=True)
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Self-contained config + utilities for this skill (no cross-skill imports).
|
|
2
|
+
Merged from the shared core; reads the same env vars as the rest of the bundle."""
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
import urllib.error
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from email.utils import parsedate_to_datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
16
|
+
_EXISTING_CONFIG_REF = globals().get("CONFIG")
|
|
17
|
+
|
|
18
|
+
DEFAULT_MIMO_API_URL = "https://api.xiaomimimo.com/v1"
|
|
19
|
+
DEFAULT_MIMO_TOKEN_PLAN_CLUSTER = "cn"
|
|
20
|
+
MIMO_TOKEN_PLAN_API_URLS = {
|
|
21
|
+
"cn": "https://token-plan-cn.xiaomimimo.com/v1",
|
|
22
|
+
"sgp": "https://token-plan-sgp.xiaomimimo.com/v1",
|
|
23
|
+
"ams": "https://token-plan-ams.xiaomimimo.com/v1",
|
|
24
|
+
}
|
|
25
|
+
DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
|
|
26
|
+
DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
|
|
27
|
+
DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
|
|
28
|
+
DEFAULT_FISH_TTS_API_URL = "https://api.fish.audio/v1/tts"
|
|
29
|
+
DEFAULT_FISH_TTS_MODEL = "s2.1-pro-free"
|
|
30
|
+
DEFAULT_FISH_TTS_REFERENCE_ID = "5653cea4ac83480aaf2bf45406556185"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_api_url(raw_url):
|
|
34
|
+
"""Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
|
|
35
|
+
url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
|
|
36
|
+
if url.endswith("/chat/completions"):
|
|
37
|
+
return url
|
|
38
|
+
return f"{url}/chat/completions"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_mimo_token_plan_key(api_key):
|
|
42
|
+
"""Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
|
|
43
|
+
return str(api_key or "").strip().startswith("tp-")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_mimo_api_url(api_key="", cluster=None):
|
|
47
|
+
"""Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
|
|
48
|
+
|
|
49
|
+
MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
|
|
50
|
+
(`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
|
|
51
|
+
not the pay-as-you-go `api.xiaomimimo.com` endpoint.
|
|
52
|
+
"""
|
|
53
|
+
if is_mimo_token_plan_key(api_key):
|
|
54
|
+
cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
|
|
55
|
+
cluster_name = str(cluster_name).strip().lower()
|
|
56
|
+
return MIMO_TOKEN_PLAN_API_URLS.get(cluster_name, MIMO_TOKEN_PLAN_API_URLS[DEFAULT_MIMO_TOKEN_PLAN_CLUSTER])
|
|
57
|
+
return DEFAULT_MIMO_API_URL
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def env_int(name, default, *, minimum=None):
|
|
61
|
+
"""Read an integer env var; ignore malformed values instead of crashing import."""
|
|
62
|
+
raw = os.environ.get(name)
|
|
63
|
+
if raw is None or raw == "":
|
|
64
|
+
return default
|
|
65
|
+
try:
|
|
66
|
+
value = int(raw)
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
return default
|
|
69
|
+
if minimum is not None:
|
|
70
|
+
value = max(minimum, value)
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def env_bool(name, default=False):
|
|
75
|
+
"""Read common boolean env var forms."""
|
|
76
|
+
raw = os.environ.get(name)
|
|
77
|
+
if raw is None or raw == "":
|
|
78
|
+
return default
|
|
79
|
+
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def env_float(name, default, *, minimum=None):
|
|
83
|
+
"""Read a float env var; ignore malformed values instead of crashing import."""
|
|
84
|
+
raw = os.environ.get(name)
|
|
85
|
+
if raw is None or raw == "":
|
|
86
|
+
return default
|
|
87
|
+
try:
|
|
88
|
+
value = float(raw)
|
|
89
|
+
except (TypeError, ValueError):
|
|
90
|
+
return default
|
|
91
|
+
if minimum is not None:
|
|
92
|
+
value = max(minimum, value)
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
|
|
97
|
+
# (MIMO_VIDEO_API_KEY / MIMO_TTS_API_KEY / MIMO_ASR_API_KEY and their *_API_URL forms)
|
|
98
|
+
# are optional and fall back to MIMO_API_KEY / MIMO_API_URL. Token-Plan keys (tp-*) auto-
|
|
99
|
+
# route to the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
|
|
100
|
+
_mimo_api_key = os.environ.get("MIMO_API_KEY", "")
|
|
101
|
+
_mimo_tts_api_key = os.environ.get("MIMO_TTS_API_KEY", "") or _mimo_api_key
|
|
102
|
+
_raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
|
|
103
|
+
_raw_mimo_tts_api_url = (
|
|
104
|
+
os.environ.get("MIMO_TTS_API_URL")
|
|
105
|
+
or os.environ.get("MIMO_API_URL")
|
|
106
|
+
or default_mimo_api_url(_mimo_tts_api_key)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
CONFIG = {
|
|
110
|
+
"api_url": normalize_api_url(_raw_api_url),
|
|
111
|
+
"api_key": _mimo_api_key,
|
|
112
|
+
"api_key_source": "MIMO_API_KEY",
|
|
113
|
+
"mimo_api_url": normalize_api_url(_raw_api_url),
|
|
114
|
+
"mimo_api_key": _mimo_api_key,
|
|
115
|
+
"mimo_tts_api_url": normalize_api_url(_raw_mimo_tts_api_url),
|
|
116
|
+
"mimo_tts_api_key": _mimo_tts_api_key,
|
|
117
|
+
"mimo_tts_api_key_source": "MIMO_TTS_API_KEY" if os.environ.get("MIMO_TTS_API_KEY") else "MIMO_API_KEY",
|
|
118
|
+
"mimo_asr_model": os.environ.get("MIMO_ASR_MODEL", DEFAULT_MIMO_ASR_MODEL),
|
|
119
|
+
"mimo_tts_model": os.environ.get("MIMO_TTS_MODEL", DEFAULT_MIMO_TTS_MODEL),
|
|
120
|
+
"mimo_tts_voice": os.environ.get("MIMO_TTS_VOICE", "冰糖"),
|
|
121
|
+
"voice_ref": os.environ.get("VOICE_REF", "").strip(), # optional arbitrary reference audio for narration voice clone
|
|
122
|
+
"mimo_tts_style": os.environ.get(
|
|
123
|
+
"MIMO_TTS_STYLE",
|
|
124
|
+
"自然、清晰、有感染力,像在给观众讲故事;随剧情起伏,该紧张时紧张、该动情时动情,不平铺直叙。",
|
|
125
|
+
),
|
|
126
|
+
"tts_provider": os.environ.get("TTS_PROVIDER", "auto").strip().lower(),
|
|
127
|
+
"tts_timeout": env_int("TTS_TIMEOUT", 300, minimum=1),
|
|
128
|
+
"fish_api_key": os.environ.get("FISH_API_KEY", ""),
|
|
129
|
+
"fish_tts_api_url": os.environ.get("FISH_TTS_API_URL", DEFAULT_FISH_TTS_API_URL),
|
|
130
|
+
"fish_tts_model": os.environ.get("FISH_TTS_MODEL", DEFAULT_FISH_TTS_MODEL),
|
|
131
|
+
"fish_tts_reference_id": os.environ.get(
|
|
132
|
+
"FISH_TTS_REFERENCE_ID", DEFAULT_FISH_TTS_REFERENCE_ID
|
|
133
|
+
).strip(),
|
|
134
|
+
"mimo_disable_thinking": env_bool("MIMO_DISABLE_THINKING", True),
|
|
135
|
+
"breath_ms": 250, # 段间呼吸空间(ms);block recap 块内连贯、块间留原声呼吸
|
|
136
|
+
"narration_speed": env_float("NARRATION_SPEED", 1.15, minimum=0.5), # 解说整体提速(atempo),默认回到可懂区间;长片可设 1.0
|
|
137
|
+
"narration_cumulative_tempo_max": env_float("NARRATION_CUMULATIVE_TEMPO_MAX", 1.35, minimum=1.0), # TTS rate × 全局 atempo × 段内 atempo 的累计上限
|
|
138
|
+
"narration_cumulative_tempo_hard_max": env_float("NARRATION_CUMULATIVE_TEMPO_HARD_MAX", 1.40, minimum=1.0), # QC/阻断硬上限
|
|
139
|
+
"tts_segment_tempo_max": env_float("TTS_SEGMENT_TEMPO_MAX", 1.20, minimum=1.0), # 兼容旧段内 atempo 上限;实际会被累计预算收紧
|
|
140
|
+
"tts_dynamic_params": True, # 启用动态语速调节
|
|
141
|
+
"tts_workers": env_int("TTS_WORKERS", 4, minimum=1), # TTS 并行合成线程数
|
|
142
|
+
"tts_retries": env_int("TTS_RETRIES", 3, minimum=1), # 单段 TTS 失败重试次数
|
|
143
|
+
"allow_partial_tts": env_bool("ALLOW_PARTIAL_TTS", False),
|
|
144
|
+
"tts_segment_normalize": env_bool("TTS_SEGMENT_NORMALIZE", True), # 单段 TTS RMS 归一,降低段间忽大忽小
|
|
145
|
+
"tts_segment_target_rms_dbfs": env_float("TTS_SEGMENT_TARGET_RMS_DBFS", -20.0),
|
|
146
|
+
"tts_segment_peak_limit": env_float("TTS_SEGMENT_PEAK_LIMIT", 0.98, minimum=0.1),
|
|
147
|
+
}
|
|
148
|
+
if isinstance(_EXISTING_CONFIG_REF, dict):
|
|
149
|
+
_EXISTING_CONFIG_REF.clear()
|
|
150
|
+
_EXISTING_CONFIG_REF.update(CONFIG)
|
|
151
|
+
CONFIG = _EXISTING_CONFIG_REF
|
|
152
|
+
|
|
153
|
+
def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
|
|
154
|
+
"""Return the canonical tempo budget shared by voiceover and assemble."""
|
|
155
|
+
cfg = config or CONFIG
|
|
156
|
+
global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
|
|
157
|
+
rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
|
|
158
|
+
cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
|
|
159
|
+
hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
|
|
160
|
+
legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
|
|
161
|
+
segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
|
|
162
|
+
return {
|
|
163
|
+
"global_narration_speed": global_speed,
|
|
164
|
+
"tts_rate_factor": rate_factor,
|
|
165
|
+
"cumulative_tempo_max": cumulative_max,
|
|
166
|
+
"cumulative_tempo_hard_max": hard_max,
|
|
167
|
+
"segment_tempo_max": segment_tempo_max,
|
|
168
|
+
"max_raw_duration_factor": global_speed * segment_tempo_max,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def log(msg):
|
|
172
|
+
print(f"[video-recap] {msg}", flush=True)
|
|
173
|
+
|
|
174
|
+
def run_cmd(cmd, **kwargs):
|
|
175
|
+
"""运行命令,返回 CompletedProcess"""
|
|
176
|
+
if isinstance(cmd, list):
|
|
177
|
+
display_parts = []
|
|
178
|
+
for part in cmd:
|
|
179
|
+
text = str(part)
|
|
180
|
+
display_parts.append(text if len(text) <= 240 else text[:237] + "...")
|
|
181
|
+
display = " ".join(display_parts)
|
|
182
|
+
else:
|
|
183
|
+
display = str(cmd)
|
|
184
|
+
if len(display) > 2000:
|
|
185
|
+
display = display[:1997] + "..."
|
|
186
|
+
log(f"运行: {display}")
|
|
187
|
+
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
|
188
|
+
|
|
189
|
+
def get_video_duration(video_path):
|
|
190
|
+
"""获取视频时长(秒)"""
|
|
191
|
+
cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
|
192
|
+
"-of", "csv=p=0", str(video_path)]
|
|
193
|
+
result = run_cmd(cmd)
|
|
194
|
+
if result.returncode != 0:
|
|
195
|
+
return 0.0
|
|
196
|
+
try:
|
|
197
|
+
return float(result.stdout.strip())
|
|
198
|
+
except (TypeError, ValueError):
|
|
199
|
+
return 0.0
|
|
200
|
+
|
|
201
|
+
def stable_json_dumps(value):
|
|
202
|
+
"""Serialize values deterministically for non-secret cache fingerprints."""
|
|
203
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
204
|
+
|
|
205
|
+
def stable_hash(value):
|
|
206
|
+
"""Return an md5 digest for deterministic JSON-serializable values."""
|
|
207
|
+
return hashlib.md5(stable_json_dumps(value).encode("utf-8")).hexdigest()
|
|
208
|
+
|
|
209
|
+
_FILE_FINGERPRINT_MEMO = {}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _file_identity(path):
|
|
213
|
+
"""(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
|
|
214
|
+
st = os.stat(os.fspath(path))
|
|
215
|
+
return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def file_fingerprint(path, chunk_size=1024 * 1024):
|
|
219
|
+
"""Return a full-content fingerprint for cache-correct identity checks.
|
|
220
|
+
|
|
221
|
+
The digest covers CONTENT only — never the path or mtime — so a copied video or
|
|
222
|
+
artifact is still recognised as the same asset, while any byte change invalidates
|
|
223
|
+
the cache even if timestamps, size, head, or tail bytes are misleading.
|
|
224
|
+
|
|
225
|
+
Identity metadata is used ONLY to memoize within a single process. One understanding
|
|
226
|
+
run fingerprints the same source video 8-10 times and the whole extracted frame set
|
|
227
|
+
2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
|
|
228
|
+
any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
|
|
229
|
+
re-hashed, so the memo can never serve a stale digest.
|
|
230
|
+
"""
|
|
231
|
+
key = _file_identity(path)
|
|
232
|
+
memoized = _FILE_FINGERPRINT_MEMO.get(key)
|
|
233
|
+
if memoized is not None:
|
|
234
|
+
return memoized
|
|
235
|
+
h = hashlib.sha256()
|
|
236
|
+
with open(os.fspath(path), "rb") as f:
|
|
237
|
+
for chunk in iter(lambda: f.read(chunk_size), b""):
|
|
238
|
+
h.update(chunk)
|
|
239
|
+
digest = h.hexdigest()
|
|
240
|
+
_FILE_FINGERPRINT_MEMO[key] = digest
|
|
241
|
+
return digest
|
|
242
|
+
def _retry_after_seconds(value, fallback):
|
|
243
|
+
"""Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
|
|
244
|
+
if not value:
|
|
245
|
+
return fallback
|
|
246
|
+
try:
|
|
247
|
+
return max(fallback, max(0, int(value)))
|
|
248
|
+
except (TypeError, ValueError):
|
|
249
|
+
pass
|
|
250
|
+
try:
|
|
251
|
+
retry_at = parsedate_to_datetime(value)
|
|
252
|
+
if retry_at.tzinfo is None:
|
|
253
|
+
retry_at = retry_at.replace(tzinfo=timezone.utc)
|
|
254
|
+
return max(fallback, max(0, int((retry_at - datetime.now(timezone.utc)).total_seconds())))
|
|
255
|
+
except (TypeError, ValueError, IndexError, OverflowError):
|
|
256
|
+
return fallback
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
_ERROR_DATA_URL_RE = re.compile(
|
|
260
|
+
r"data:(?:audio|video|image)/[^;,\s\"'<>]+;base64,[A-Za-z0-9+/=]+",
|
|
261
|
+
re.IGNORECASE,
|
|
262
|
+
)
|
|
263
|
+
_ERROR_KEY_RE = re.compile(r"\b(?:tp|sk)-[A-Za-z0-9_-]{8,}\b")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _sanitize_api_error(value, limit=500, *, extra_secrets=()):
|
|
267
|
+
"""Bound transport diagnostics without echoing request media or credentials."""
|
|
268
|
+
text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value or ""))
|
|
269
|
+
text = _ERROR_KEY_RE.sub("<redacted-key>", text)
|
|
270
|
+
for secret in extra_secrets:
|
|
271
|
+
if secret:
|
|
272
|
+
text = text.replace(str(secret), "<redacted-key>")
|
|
273
|
+
return text[:limit]
|
|
274
|
+
|
|
275
|
+
def _api_headers(api_provider=None, api_url=None, api_key=None):
|
|
276
|
+
"""Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
|
|
277
|
+
del api_provider, api_url # MiMo is the only provider; signature kept for call sites
|
|
278
|
+
key = CONFIG.get("api_key", "") if api_key is None else api_key
|
|
279
|
+
return {
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
"User-Agent": "video-recap/1.0",
|
|
282
|
+
"api-key": key,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
def _prepare_api_payload(payload, api_provider=None, api_url=None):
|
|
286
|
+
"""Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
|
|
287
|
+
del api_provider, api_url
|
|
288
|
+
normalized = dict(payload)
|
|
289
|
+
if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
|
|
290
|
+
normalized["max_completion_tokens"] = normalized.pop("max_tokens")
|
|
291
|
+
model = str(normalized.get("model") or "")
|
|
292
|
+
if (
|
|
293
|
+
CONFIG.get("mimo_disable_thinking", True)
|
|
294
|
+
and not model.endswith(("-tts", "-asr"))
|
|
295
|
+
and "thinking" not in normalized
|
|
296
|
+
):
|
|
297
|
+
# MiMo V2.5 may spend small max_completion_tokens budgets on reasoning_content.
|
|
298
|
+
# The recap pipeline needs visible text, so disable thinking unless set explicitly.
|
|
299
|
+
normalized["thinking"] = {"type": "disabled"}
|
|
300
|
+
return normalized
|
|
301
|
+
|
|
302
|
+
def _mimo_endpoint(kind):
|
|
303
|
+
"""Return per-capability MiMo endpoint settings (video understanding / TTS / ASR)."""
|
|
304
|
+
by_kind = {
|
|
305
|
+
"video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_api_key_source"),
|
|
306
|
+
"tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_api_key_source"),
|
|
307
|
+
"asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_api_key_source"),
|
|
308
|
+
}
|
|
309
|
+
if kind not in by_kind:
|
|
310
|
+
raise ValueError(f"Unsupported MiMo endpoint kind: {kind}")
|
|
311
|
+
url_key, key_key, src_key = by_kind[kind]
|
|
312
|
+
return {
|
|
313
|
+
"api_url": CONFIG.get(url_key) or CONFIG.get("mimo_api_url"),
|
|
314
|
+
"api_key": CONFIG.get(key_key) or CONFIG.get("mimo_api_key"),
|
|
315
|
+
"api_key_source": CONFIG.get(src_key, "MIMO_API_KEY"),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
def _call_mimo_endpoint(kind, payload, max_retries=10):
|
|
319
|
+
settings = _mimo_endpoint(kind)
|
|
320
|
+
return api_call(
|
|
321
|
+
payload,
|
|
322
|
+
max_retries=max_retries,
|
|
323
|
+
api_provider="mimo",
|
|
324
|
+
api_url=settings["api_url"],
|
|
325
|
+
api_key=settings["api_key"],
|
|
326
|
+
api_key_source=settings["api_key_source"],
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def mimo_tts_api_call(payload, max_retries=10):
|
|
330
|
+
"""Call the MiMo TTS endpoint."""
|
|
331
|
+
return _call_mimo_endpoint("tts", payload, max_retries=max_retries)
|
|
332
|
+
|
|
333
|
+
def mimo_asr_api_call(payload, max_retries=10):
|
|
334
|
+
"""Call the MiMo speech-recognition (ASR) endpoint."""
|
|
335
|
+
return _call_mimo_endpoint("asr", payload, max_retries=max_retries)
|
|
336
|
+
|
|
337
|
+
def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_key_source=None):
|
|
338
|
+
"""调用 OpenAI-compatible API,带重试。
|
|
339
|
+
|
|
340
|
+
集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
|
|
341
|
+
避免一次瞬时限流就中止整个阶段。配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s。
|
|
342
|
+
"""
|
|
343
|
+
endpoint = normalize_api_url(api_url if api_url is not None else CONFIG["api_url"])
|
|
344
|
+
headers = _api_headers(api_provider=api_provider, api_url=endpoint, api_key=api_key)
|
|
345
|
+
data = json.dumps(_prepare_api_payload(payload, api_provider=api_provider, api_url=endpoint)).encode("utf-8")
|
|
346
|
+
|
|
347
|
+
for attempt in range(max_retries):
|
|
348
|
+
try:
|
|
349
|
+
req = urllib.request.Request(endpoint, data=data, headers=headers)
|
|
350
|
+
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
351
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
352
|
+
return result
|
|
353
|
+
except urllib.error.HTTPError as e:
|
|
354
|
+
body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
|
|
355
|
+
wait = min(2 ** attempt, 60)
|
|
356
|
+
if e.code == 429:
|
|
357
|
+
retry_after = e.headers.get("Retry-After")
|
|
358
|
+
wait = _retry_after_seconds(retry_after, max(wait, 10))
|
|
359
|
+
log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
|
|
360
|
+
elif e.code == 401:
|
|
361
|
+
key_name = api_key_source or CONFIG.get("api_key_source", "MIMO_API_KEY")
|
|
362
|
+
raise RuntimeError(f"API 认证失败 (401)。请检查 {key_name} 和 API URL 是否匹配。")
|
|
363
|
+
elif e.code == 403:
|
|
364
|
+
hint = "API 访问被拒绝 (403)。"
|
|
365
|
+
if "1010" in body or "cloudflare" in body.lower():
|
|
366
|
+
hint += "IP 被 Cloudflare 限流,请等待几分钟后重试。"
|
|
367
|
+
raise RuntimeError(hint)
|
|
368
|
+
hint += "请检查 API key 权限和 API URL 设置。"
|
|
369
|
+
raise RuntimeError(hint)
|
|
370
|
+
elif e.code == 405:
|
|
371
|
+
raise RuntimeError("API 端点不可用 (405),可能被 WAF 拦截。请检查 MIMO_API_URL 或稍后重试。")
|
|
372
|
+
elif e.code == 503:
|
|
373
|
+
log(f"API 服务暂不可用 (503),等待 {wait}s (尝试 {attempt+1}/{max_retries})")
|
|
374
|
+
elif e.code == 524:
|
|
375
|
+
# Cloudflare 超时:服务端处理超时,需要更长退避
|
|
376
|
+
wait = max(wait, 4 * (attempt + 1))
|
|
377
|
+
log(f"API 超时 (524),等待 {wait}s (尝试 {attempt+1}/{max_retries})")
|
|
378
|
+
else:
|
|
379
|
+
log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): HTTP {e.code} — {body}")
|
|
380
|
+
if attempt < max_retries - 1:
|
|
381
|
+
time.sleep(wait)
|
|
382
|
+
else:
|
|
383
|
+
raise RuntimeError(f"API 调用失败 {max_retries} 次: HTTP {e.code} — {body}")
|
|
384
|
+
except Exception as e: # noqa: BLE001 - transport/decode faults are all retryable here
|
|
385
|
+
# (Deliberately broad, but no longer written as `(URLError, Exception)`, which
|
|
386
|
+
# read as a tuple while `Exception` already subsumed the first member.)
|
|
387
|
+
wait = min(2 ** attempt, 60)
|
|
388
|
+
safe_error = _sanitize_api_error(e)
|
|
389
|
+
log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): {safe_error}")
|
|
390
|
+
if attempt < max_retries - 1:
|
|
391
|
+
log(f"等待 {wait}s 后重试...")
|
|
392
|
+
time.sleep(wait)
|
|
393
|
+
else:
|
|
394
|
+
raise RuntimeError(f"API 调用失败 {max_retries} 次: {safe_error}")
|
|
395
|
+
# Unreachable for max_retries >= 1; guards against a silent `None` return (and the
|
|
396
|
+
# TypeError it would cause at the caller's resp["choices"]) if a caller passes 0.
|
|
397
|
+
raise ValueError(f"max_retries must be >= 1, got {max_retries}")
|
|
398
|
+
|
|
399
|
+
def _text_char_count(text):
|
|
400
|
+
"""计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
|
|
401
|
+
return len(re.sub(r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]', '', text or ""))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _truncate_at_sentence(text, max_chars):
|
|
405
|
+
"""在句子边界截断,不产生残句。max_chars 按有效字符计(不含标点空白)。"""
|
|
406
|
+
if _text_char_count(text) <= max_chars:
|
|
407
|
+
return text
|
|
408
|
+
eff = 0
|
|
409
|
+
cutoff = len(text)
|
|
410
|
+
for i, ch in enumerate(text):
|
|
411
|
+
eff += 1 if _text_char_count(ch) else 0
|
|
412
|
+
if eff > max_chars:
|
|
413
|
+
cutoff = i + 1
|
|
414
|
+
break
|
|
415
|
+
idx = max(text[:cutoff].rfind(sep) for sep in ['。', '!', '?', '!', '?'])
|
|
416
|
+
if idx > 0:
|
|
417
|
+
return text[:idx + 1]
|
|
418
|
+
idx = max(text[:cutoff].rfind(sep) for sep in [',', '、', ';', ','])
|
|
419
|
+
if idx > 3:
|
|
420
|
+
return text[:idx] + '。'
|
|
421
|
+
return ""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Loudness normalization for synthesized TTS blocks.
|
|
2
|
+
|
|
3
|
+
Split out of voiceover.py to keep that module within the bundle's per-module line
|
|
4
|
+
budget. Dependency-free on purpose (stdlib `wave` + `array` only) so QC and assembly
|
|
5
|
+
can reuse the returned metadata without pulling in the synthesis path.
|
|
6
|
+
"""
|
|
7
|
+
import array
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import wave
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from lib import CONFIG, log
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak_limit=0.98):
|
|
18
|
+
"""Normalize a mono/stereo 16-bit WAV to a target RMS with peak guard.
|
|
19
|
+
|
|
20
|
+
This helper is intentionally dependency-free so QC/assembly can reuse the
|
|
21
|
+
returned metadata even when normalization is applied in a later lane.
|
|
22
|
+
"""
|
|
23
|
+
input_wav = Path(input_wav)
|
|
24
|
+
output_wav = Path(output_wav)
|
|
25
|
+
with wave.open(str(input_wav), "rb") as wf:
|
|
26
|
+
channels = wf.getnchannels()
|
|
27
|
+
sampwidth = wf.getsampwidth()
|
|
28
|
+
framerate = wf.getframerate()
|
|
29
|
+
frames = wf.getnframes()
|
|
30
|
+
data = wf.readframes(frames)
|
|
31
|
+
if sampwidth != 2:
|
|
32
|
+
raise ValueError(f"仅支持 16-bit PCM WAV: {input_wav}")
|
|
33
|
+
|
|
34
|
+
# array('h') decodes the whole block in C. The previous per-sample int.from_bytes
|
|
35
|
+
# comprehension ran three times over the audio (decode, normalize, re-decode the
|
|
36
|
+
# OUTPUT just to measure it) — about 420ms for a 12s block, times every narration
|
|
37
|
+
# segment. Same arithmetic, same rounding, one decode.
|
|
38
|
+
samples = array.array("h")
|
|
39
|
+
samples.frombytes(data[: len(data) - (len(data) % 2)])
|
|
40
|
+
if sys.byteorder != "little": # wave data is little-endian regardless of host
|
|
41
|
+
samples.byteswap()
|
|
42
|
+
if not samples:
|
|
43
|
+
output_wav.write_bytes(input_wav.read_bytes())
|
|
44
|
+
return {
|
|
45
|
+
"rms_dbfs_before": None,
|
|
46
|
+
"rms_dbfs_after": None,
|
|
47
|
+
"peak_after": 0.0,
|
|
48
|
+
"gain_db": 0.0,
|
|
49
|
+
}
|
|
50
|
+
count = len(samples)
|
|
51
|
+
rms = math.sqrt(sum(s * s for s in samples) / count)
|
|
52
|
+
peak = max(max(samples), -min(samples)) / 32768.0
|
|
53
|
+
rms_dbfs_before = 20 * math.log10(max(rms, 1e-9) / 32768.0)
|
|
54
|
+
target_linear = 10 ** (float(target_rms_dbfs) / 20.0) * 32768.0
|
|
55
|
+
gain = target_linear / max(rms, 1e-9)
|
|
56
|
+
if peak > 0:
|
|
57
|
+
gain = min(gain, float(peak_limit) / peak)
|
|
58
|
+
|
|
59
|
+
# Accumulate the output statistics while normalizing instead of decoding the result back.
|
|
60
|
+
normalized = array.array("h", bytes(2 * count))
|
|
61
|
+
out_square_sum = 0
|
|
62
|
+
out_peak_raw = 0
|
|
63
|
+
for i in range(count):
|
|
64
|
+
value = max(-32768, min(32767, int(round(samples[i] * gain))))
|
|
65
|
+
normalized[i] = value
|
|
66
|
+
out_square_sum += value * value
|
|
67
|
+
if abs(value) > out_peak_raw:
|
|
68
|
+
out_peak_raw = abs(value)
|
|
69
|
+
out_bytes = normalized
|
|
70
|
+
if sys.byteorder != "little":
|
|
71
|
+
out_bytes = array.array("h", normalized)
|
|
72
|
+
out_bytes.byteswap()
|
|
73
|
+
with wave.open(str(output_wav), "wb") as wf:
|
|
74
|
+
wf.setnchannels(channels)
|
|
75
|
+
wf.setsampwidth(sampwidth)
|
|
76
|
+
wf.setframerate(framerate)
|
|
77
|
+
wf.writeframes(out_bytes.tobytes())
|
|
78
|
+
|
|
79
|
+
out_rms = math.sqrt(out_square_sum / count)
|
|
80
|
+
return {
|
|
81
|
+
"rms_dbfs_before": rms_dbfs_before,
|
|
82
|
+
"rms_dbfs_after": 20 * math.log10(max(out_rms, 1e-9) / 32768.0),
|
|
83
|
+
"peak_after": out_peak_raw / 32768.0,
|
|
84
|
+
"gain_db": 20 * math.log10(max(gain, 1e-9)),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _maybe_normalize_tts_wav(output_wav):
|
|
89
|
+
"""Normalize a synthesized TTS block in-place when possible.
|
|
90
|
+
|
|
91
|
+
Unit tests often stub TTS with text bytes rather than real WAV. In that case
|
|
92
|
+
normalization is skipped safely; real MiMo WAV output gets RMS/peak metadata.
|
|
93
|
+
"""
|
|
94
|
+
if not CONFIG.get("tts_segment_normalize", True):
|
|
95
|
+
return None
|
|
96
|
+
output_wav = Path(output_wav)
|
|
97
|
+
tmp = output_wav.with_name(f"{output_wav.stem}_norm{output_wav.suffix}")
|
|
98
|
+
try:
|
|
99
|
+
meta = _normalize_tts_wav_rms(
|
|
100
|
+
output_wav,
|
|
101
|
+
tmp,
|
|
102
|
+
target_rms_dbfs=float(CONFIG.get("tts_segment_target_rms_dbfs", -20.0) or -20.0),
|
|
103
|
+
peak_limit=float(CONFIG.get("tts_segment_peak_limit", 0.98) or 0.98),
|
|
104
|
+
)
|
|
105
|
+
os.replace(tmp, output_wav)
|
|
106
|
+
return meta
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
try:
|
|
109
|
+
tmp.unlink(missing_ok=True)
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
log(f" TTS RMS 归一跳过: {exc}")
|
|
113
|
+
return None
|