@oh-story/dsh 0.1.4 → 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 +56 -12
- package/lib/client.js +2692 -268
- package/lib/client.js.map +4 -4
- package/lib/drama/manifest.json +136 -101
- package/lib/drama/skills/short-drama/SKILL.md +61 -1
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
- package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
- package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
- package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
- package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
- 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 +13 -3
- 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 +2 -1
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
- 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 +46 -10
- 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/references/rubric-visual-motion.md +12 -0
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
- 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 +5 -2
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
- 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/delivery-profile.md +21 -1
- 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 +24 -6
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
- 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 +5 -2
- 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 +3 -2
- 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 +865 -33
- 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 +415 -305
- package/lib/oh-story/roles/consistency-checker.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +32 -33
- package/lib/oh-story/roles/story-architect.md +16 -26
- 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 +19 -10
- 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-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/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 +4 -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 +14 -14
- 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 +74 -33
- 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-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/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-setup/references/agent-references/{reversal-toolkit.md → 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-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/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 +4 -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 +3 -1
- 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/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 +55 -40
- 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,410 @@
|
|
|
1
|
+
"""Normalize, split, budget, and de-duplicate narration text."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from lib import CONFIG
|
|
13
|
+
|
|
14
|
+
from lib import log
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from deslop_qc import analyze_deslop_qc
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
_deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
|
|
20
|
+
_deslop_qc_spec = importlib.util.spec_from_file_location(
|
|
21
|
+
"deslop_qc", _deslop_qc_path
|
|
22
|
+
)
|
|
23
|
+
if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
|
|
24
|
+
raise
|
|
25
|
+
_deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
|
|
26
|
+
_deslop_qc_spec.loader.exec_module(_deslop_qc_module)
|
|
27
|
+
analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format_frame_facts(scene):
|
|
31
|
+
"""将帧动作描述格式化为可注入 agent brief 的文本。"""
|
|
32
|
+
facts = scene.get("frame_facts", {})
|
|
33
|
+
if not facts:
|
|
34
|
+
return ""
|
|
35
|
+
lines = []
|
|
36
|
+
for ts in sorted(facts.keys(), key=lambda x: float(x)):
|
|
37
|
+
actions = facts[ts]
|
|
38
|
+
lines.append(f" {ts}s: {'; '.join(actions)}")
|
|
39
|
+
return "\n 帧动作:\n" + "\n".join(lines)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _text_char_count(text):
|
|
43
|
+
"""计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
|
|
44
|
+
return len(
|
|
45
|
+
re.sub(
|
|
46
|
+
r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]',
|
|
47
|
+
"",
|
|
48
|
+
text or "",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _contains_cjk(text):
|
|
54
|
+
return bool(re.search(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]", text or ""))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _writing_text_units(text):
|
|
58
|
+
"""Length unit used for ASR writing chunks: CJK chars, otherwise words."""
|
|
59
|
+
text = str(text or "")
|
|
60
|
+
if _contains_cjk(text):
|
|
61
|
+
return len(re.sub(r"\s+", "", text))
|
|
62
|
+
return len(re.findall(r"\b\w+\b", text))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sentence_pieces(text):
|
|
66
|
+
"""Split text into sentence-like pieces while keeping terminal punctuation."""
|
|
67
|
+
text = re.sub(r"\s+", " ", str(text or "")).strip()
|
|
68
|
+
if not text:
|
|
69
|
+
return []
|
|
70
|
+
parts = re.split(r"([。!?!?;;.])", text)
|
|
71
|
+
cleaned = []
|
|
72
|
+
for idx in range(0, len(parts), 2):
|
|
73
|
+
body = parts[idx].strip()
|
|
74
|
+
punct = parts[idx + 1] if idx + 1 < len(parts) else ""
|
|
75
|
+
if body or punct:
|
|
76
|
+
cleaned.append((body + punct).strip())
|
|
77
|
+
return cleaned or [text]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _split_text_by_sentence_windows(text, min_chars=500, max_chars=800):
|
|
81
|
+
"""Clipto-style three-tier sentence boundary splitting for long ASR text."""
|
|
82
|
+
text = re.sub(r"\s+", " ", str(text or "")).strip()
|
|
83
|
+
if not text:
|
|
84
|
+
return []
|
|
85
|
+
if _writing_text_units(text) <= max_chars:
|
|
86
|
+
return _sentence_pieces(text)
|
|
87
|
+
|
|
88
|
+
result = []
|
|
89
|
+
rest = text
|
|
90
|
+
sentence_marks = "。!?!?;;."
|
|
91
|
+
while _writing_text_units(rest) > max_chars:
|
|
92
|
+
# The min/max thresholds are unit-based but punctuation is char-indexed.
|
|
93
|
+
# For Chinese (the dominant recap target) these are nearly identical; for
|
|
94
|
+
# non-CJK this remains a safe sentence-boundary heuristic around words.
|
|
95
|
+
char_min = min(len(rest), max(1, min_chars))
|
|
96
|
+
char_max = min(len(rest), max(1, max_chars))
|
|
97
|
+
window = rest[:char_max]
|
|
98
|
+
cut = max(window.rfind(mark) for mark in sentence_marks)
|
|
99
|
+
if cut + 1 < char_min:
|
|
100
|
+
outside = -1
|
|
101
|
+
for i, ch in enumerate(rest[char_max:], start=char_max):
|
|
102
|
+
if ch in sentence_marks:
|
|
103
|
+
outside = i
|
|
104
|
+
break
|
|
105
|
+
if outside >= 0 and outside + 1 <= len(rest):
|
|
106
|
+
cut = outside
|
|
107
|
+
else:
|
|
108
|
+
cut = char_max - 1
|
|
109
|
+
piece = rest[: cut + 1].strip()
|
|
110
|
+
if not piece:
|
|
111
|
+
piece = rest[:char_max].strip()
|
|
112
|
+
cut = char_max - 1
|
|
113
|
+
result.append(piece)
|
|
114
|
+
rest = rest[cut + 1 :].strip()
|
|
115
|
+
if rest:
|
|
116
|
+
result.append(rest)
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _timed_sentence_pieces(seg, min_chars, max_chars):
|
|
121
|
+
"""Split one ASR segment into timed sentence pieces with approximate spans."""
|
|
122
|
+
text = str(seg.get("text", "")).strip()
|
|
123
|
+
if not text:
|
|
124
|
+
return []
|
|
125
|
+
try:
|
|
126
|
+
start = float(seg.get("start", 0))
|
|
127
|
+
end = float(seg.get("end", start))
|
|
128
|
+
except (TypeError, ValueError):
|
|
129
|
+
start = end = 0.0
|
|
130
|
+
if end < start:
|
|
131
|
+
end = start
|
|
132
|
+
pieces = []
|
|
133
|
+
for sentence in _sentence_pieces(text):
|
|
134
|
+
if _writing_text_units(sentence) > max_chars:
|
|
135
|
+
pieces.extend(
|
|
136
|
+
_split_text_by_sentence_windows(
|
|
137
|
+
sentence, min_chars=min_chars, max_chars=max_chars
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
pieces.append(sentence)
|
|
142
|
+
total_units = sum(max(1, _writing_text_units(piece)) for piece in pieces) or 1
|
|
143
|
+
duration = max(0.0, end - start)
|
|
144
|
+
cursor = start
|
|
145
|
+
timed = []
|
|
146
|
+
for idx, piece in enumerate(pieces):
|
|
147
|
+
units = max(1, _writing_text_units(piece))
|
|
148
|
+
piece_duration = duration * units / total_units if duration else 0.0
|
|
149
|
+
piece_end = end if idx == len(pieces) - 1 else cursor + piece_duration
|
|
150
|
+
timed.append(
|
|
151
|
+
{
|
|
152
|
+
"start": round(cursor, 2),
|
|
153
|
+
"end": round(piece_end, 2),
|
|
154
|
+
"text": piece,
|
|
155
|
+
"char_count": _writing_text_units(piece),
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
cursor = piece_end
|
|
159
|
+
return timed
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _scene_ids_for_range(scenes, start, end):
|
|
163
|
+
scene_ids = []
|
|
164
|
+
duration = max(0.001, float(end) - float(start))
|
|
165
|
+
for scene in scenes or []:
|
|
166
|
+
try:
|
|
167
|
+
s_start = float(scene.get("start", 0))
|
|
168
|
+
s_end = float(scene.get("end", s_start))
|
|
169
|
+
except (TypeError, ValueError):
|
|
170
|
+
continue
|
|
171
|
+
overlap = _overlap_seconds(start, end, s_start, s_end)
|
|
172
|
+
# Ignore tiny boundary tails from approximate ASR sentence timing. A
|
|
173
|
+
# scene id should mean the chunk materially belongs to that scene.
|
|
174
|
+
if overlap and (overlap >= duration * 0.2 or overlap >= 3.0):
|
|
175
|
+
scene_ids.append(scene.get("scene_id"))
|
|
176
|
+
return [sid for sid in scene_ids if sid is not None]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _chunk_asr_for_writing(
|
|
180
|
+
segments, scenes_analysis=None, min_chars=None, max_chars=None
|
|
181
|
+
):
|
|
182
|
+
"""Chunk ASR into semantic windows before an agent writes long-dialogue recaps.
|
|
183
|
+
|
|
184
|
+
The strategy mirrors Clipto's segment splitter: accumulate a window, prefer
|
|
185
|
+
the last sentence boundary inside max length, allow a slightly longer first
|
|
186
|
+
boundary outside the window, and fall back to the remaining text. CJK text is
|
|
187
|
+
measured by characters; non-CJK text is measured by words.
|
|
188
|
+
"""
|
|
189
|
+
min_chars = int(min_chars or CONFIG.get("asr_chunk_min_chars", 500))
|
|
190
|
+
max_chars = int(max_chars or CONFIG.get("asr_chunk_max_chars", 800))
|
|
191
|
+
min_chars = max(1, min(min_chars, max_chars))
|
|
192
|
+
max_chars = max(min_chars, max_chars)
|
|
193
|
+
|
|
194
|
+
pieces = []
|
|
195
|
+
for seg in segments or []:
|
|
196
|
+
if isinstance(seg, dict):
|
|
197
|
+
pieces.extend(_timed_sentence_pieces(seg, min_chars, max_chars))
|
|
198
|
+
|
|
199
|
+
chunks = []
|
|
200
|
+
current = []
|
|
201
|
+
current_units = 0
|
|
202
|
+
current_scene_ids = set()
|
|
203
|
+
|
|
204
|
+
def flush():
|
|
205
|
+
nonlocal current, current_units, current_scene_ids
|
|
206
|
+
if not current:
|
|
207
|
+
return
|
|
208
|
+
chunks.append(
|
|
209
|
+
{
|
|
210
|
+
"chunk_id": len(chunks),
|
|
211
|
+
"start": round(float(current[0]["start"]), 2),
|
|
212
|
+
"end": round(float(current[-1]["end"]), 2),
|
|
213
|
+
"scene_ids": sorted(
|
|
214
|
+
current_scene_ids, key=lambda sid: (isinstance(sid, str), sid)
|
|
215
|
+
),
|
|
216
|
+
"char_count": current_units,
|
|
217
|
+
"text": " ".join(piece["text"] for piece in current).strip(),
|
|
218
|
+
"segments": current,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
current = []
|
|
222
|
+
current_units = 0
|
|
223
|
+
current_scene_ids = set()
|
|
224
|
+
|
|
225
|
+
for piece in pieces:
|
|
226
|
+
units = max(
|
|
227
|
+
1, int(piece.get("char_count", _writing_text_units(piece.get("text", ""))))
|
|
228
|
+
)
|
|
229
|
+
piece_scene_ids = set(
|
|
230
|
+
_scene_ids_for_range(scenes_analysis, piece["start"], piece["end"])
|
|
231
|
+
)
|
|
232
|
+
crosses_scene = (
|
|
233
|
+
current
|
|
234
|
+
and current_scene_ids
|
|
235
|
+
and piece_scene_ids
|
|
236
|
+
and not (current_scene_ids & piece_scene_ids)
|
|
237
|
+
)
|
|
238
|
+
if crosses_scene and current_units >= min_chars:
|
|
239
|
+
flush()
|
|
240
|
+
if current and current_units >= min_chars and current_units + units > max_chars:
|
|
241
|
+
flush()
|
|
242
|
+
current.append(piece)
|
|
243
|
+
current_scene_ids.update(piece_scene_ids)
|
|
244
|
+
current_units += units
|
|
245
|
+
if current_units >= max_chars:
|
|
246
|
+
flush()
|
|
247
|
+
flush()
|
|
248
|
+
return chunks
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _truncate_at_sentence(text, max_chars):
|
|
252
|
+
"""在句子边界截断,不产生残句。max_chars 按有效字符计(不含标点空白)。"""
|
|
253
|
+
if _text_char_count(text) <= max_chars:
|
|
254
|
+
return text
|
|
255
|
+
eff = 0
|
|
256
|
+
cutoff = len(text)
|
|
257
|
+
for i, ch in enumerate(text):
|
|
258
|
+
eff += 1 if _text_char_count(ch) else 0
|
|
259
|
+
if eff > max_chars:
|
|
260
|
+
cutoff = i + 1
|
|
261
|
+
break
|
|
262
|
+
idx = max(text[:cutoff].rfind(sep) for sep in ["。", "!", "?", "!", "?"])
|
|
263
|
+
if idx > 0:
|
|
264
|
+
return text[: idx + 1]
|
|
265
|
+
idx = max(text[:cutoff].rfind(sep) for sep in [",", "、", ";", ","])
|
|
266
|
+
if idx > 3:
|
|
267
|
+
return text[:idx] + "。"
|
|
268
|
+
return ""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _char_bigrams(text):
|
|
272
|
+
return {text[i : i + 2] for i in range(len(text) - 1) if text[i : i + 2].strip()}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _post_dedup_narration(narration):
|
|
276
|
+
"""去除相邻相似解说段(bigram 重叠 >60% 则合并)。"""
|
|
277
|
+
if len(narration) < 2:
|
|
278
|
+
return narration
|
|
279
|
+
result = [narration[0]]
|
|
280
|
+
for seg in narration[1:]:
|
|
281
|
+
prev = result[-1]
|
|
282
|
+
if (
|
|
283
|
+
not prev.get("narration", "").strip()
|
|
284
|
+
or not seg.get("narration", "").strip()
|
|
285
|
+
):
|
|
286
|
+
result.append(seg)
|
|
287
|
+
continue
|
|
288
|
+
set_a, set_b = _char_bigrams(prev["narration"]), _char_bigrams(seg["narration"])
|
|
289
|
+
if not set_a or not set_b:
|
|
290
|
+
result.append(seg)
|
|
291
|
+
continue
|
|
292
|
+
overlap = len(set_a & set_b) / min(len(set_a), len(set_b))
|
|
293
|
+
# Only merge near-identical adjacent beats. Short Chinese beats share many
|
|
294
|
+
# bigrams by chance, so a low threshold collapses intentional parallel beats
|
|
295
|
+
# ("他不再试探" / "他直接赌上全力") and silently drops density below target.
|
|
296
|
+
if overlap > 0.6:
|
|
297
|
+
# Validation is also the handoff boundary for renderer metadata. When two
|
|
298
|
+
# near-identical narration beats are merged, retain overlays authored on either
|
|
299
|
+
# beat instead of silently discarding the second beat's visual instructions.
|
|
300
|
+
merged_overlays = []
|
|
301
|
+
for candidate in (prev, seg):
|
|
302
|
+
raw_overlays = candidate.get("visual_overlays")
|
|
303
|
+
if isinstance(raw_overlays, list):
|
|
304
|
+
merged_overlays.extend(copy.deepcopy(raw_overlays))
|
|
305
|
+
if len(seg["narration"]) > len(prev["narration"]):
|
|
306
|
+
prev["narration"] = seg["narration"]
|
|
307
|
+
prev["end"] = seg["end"]
|
|
308
|
+
prev["pause_after_ms"] = seg.get(
|
|
309
|
+
"pause_after_ms", prev.get("pause_after_ms", 600)
|
|
310
|
+
)
|
|
311
|
+
if merged_overlays:
|
|
312
|
+
prev["visual_overlays"] = merged_overlays
|
|
313
|
+
log(f" 去重合并: {prev['start']:.0f}-{prev['end']:.0f}s")
|
|
314
|
+
else:
|
|
315
|
+
result.append(seg)
|
|
316
|
+
removed = len(narration) - len(result)
|
|
317
|
+
if removed:
|
|
318
|
+
log(f" 去重: {len(narration)} → {len(result)} 段 (合并 {removed} 段)")
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _scene_available_seconds(start, end, pause_after_ms=None):
|
|
323
|
+
del pause_after_ms # pause_after_ms affects the next segment gap during assembly, not current speech capacity.
|
|
324
|
+
tail_pad = max(0.0, float(CONFIG.get("narration_tail_pad_seconds", 0.1) or 0.0))
|
|
325
|
+
return max(0.0, float(end) - float(start) - tail_pad)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _recommended_char_budget(start, end, pause_after_ms=None):
|
|
329
|
+
# account for the global narration atempo (CONFIG['narration_speed']) so a beat's text
|
|
330
|
+
# is budgeted against the FINAL sped-up audio, not the raw TTS rate — otherwise windows
|
|
331
|
+
# are over-sized and the bed shows long silent gaps between sentences.
|
|
332
|
+
effective_rate = (
|
|
333
|
+
CONFIG["speech_rate"]
|
|
334
|
+
* CONFIG["speech_safety_margin"]
|
|
335
|
+
* float(CONFIG.get("narration_speed", 1.0) or 1.0)
|
|
336
|
+
)
|
|
337
|
+
available = _scene_available_seconds(start, end, pause_after_ms)
|
|
338
|
+
return max(0, int(available * effective_rate))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _find_scene_for_midpoint(scenes_analysis, start, end):
|
|
342
|
+
mid = (float(start) + float(end)) / 2
|
|
343
|
+
for scene in scenes_analysis:
|
|
344
|
+
if scene["start"] <= mid <= scene["end"]:
|
|
345
|
+
return scene
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _normalise_narration_segment(seg, scenes_analysis=None):
|
|
350
|
+
if not isinstance(seg, dict):
|
|
351
|
+
return None
|
|
352
|
+
try:
|
|
353
|
+
start = float(seg.get("start"))
|
|
354
|
+
end = float(seg.get("end"))
|
|
355
|
+
except (TypeError, ValueError):
|
|
356
|
+
return None
|
|
357
|
+
if end <= start:
|
|
358
|
+
return None
|
|
359
|
+
text = str(seg.get("narration", "")).strip()
|
|
360
|
+
if not text:
|
|
361
|
+
return None
|
|
362
|
+
pause = seg.get("pause_after_ms", CONFIG.get("breath_ms", 250))
|
|
363
|
+
try:
|
|
364
|
+
pause = int(pause)
|
|
365
|
+
except (TypeError, ValueError):
|
|
366
|
+
pause = CONFIG.get("breath_ms", 250)
|
|
367
|
+
item = {
|
|
368
|
+
"start": round(start, 2),
|
|
369
|
+
"end": round(end, 2),
|
|
370
|
+
"narration": text,
|
|
371
|
+
"pause_after_ms": pause,
|
|
372
|
+
"overlaps_speech": bool(seg.get("overlaps_speech", True)),
|
|
373
|
+
}
|
|
374
|
+
for optional_key in (
|
|
375
|
+
"source_start",
|
|
376
|
+
"source_end",
|
|
377
|
+
"source_clip_id",
|
|
378
|
+
"source_entry_policy",
|
|
379
|
+
"source_entry_reason",
|
|
380
|
+
):
|
|
381
|
+
if optional_key in seg:
|
|
382
|
+
item[optional_key] = seg[optional_key]
|
|
383
|
+
# carry the per-beat emotion/tone tag (MiMo TTS instruct) through lint untouched
|
|
384
|
+
emotion = seg.get("emotion")
|
|
385
|
+
if isinstance(emotion, str) and emotion.strip():
|
|
386
|
+
item["emotion"] = emotion.strip()
|
|
387
|
+
# Preserve renderer-owned metadata across the full-mode validation rewrite. The recap
|
|
388
|
+
# orchestrator filters supported overlay kinds later; validation must not erase authored
|
|
389
|
+
# overlays merely because it normalizes narration timing/text fields.
|
|
390
|
+
visual_overlays = seg.get("visual_overlays")
|
|
391
|
+
if isinstance(visual_overlays, list):
|
|
392
|
+
item["visual_overlays"] = copy.deepcopy(visual_overlays)
|
|
393
|
+
# Authored timing is a delivery contract. Scene boundaries are approximate
|
|
394
|
+
# visual-analysis buckets, while source sentence anchors are measured audio
|
|
395
|
+
# handoff points. Lint may warn when a block crosses a scene, but validation
|
|
396
|
+
# must never silently clamp start/end and invalidate an audio-safe handoff.
|
|
397
|
+
return item
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _clean_narration_punctuation(text):
|
|
401
|
+
text = re.sub(r"\s+", " ", text or "").strip()
|
|
402
|
+
text = re.sub(r'[,:、;,]["\']?[。!?]', "。", text)
|
|
403
|
+
text = re.sub(r'["\']。$', "。", text)
|
|
404
|
+
return text
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _overlap_seconds(start, end, other_start, other_end):
|
|
408
|
+
return max(
|
|
409
|
+
0.0, min(float(end), float(other_end)) - max(float(start), float(other_start))
|
|
410
|
+
)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from lib import CONFIG
|
|
9
|
+
from lib import log, run_cmd, get_video_duration, mimo_asr_api_call, file_fingerprint
|
|
10
|
+
|
|
11
|
+
# ── Step 3: ASR 转录(MiMo mimo-v2.5-asr,云端 API)────────────────────────
|
|
12
|
+
|
|
13
|
+
_ASR_AUDIO_MIME = "audio/wav"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_name_glossary(work_dir):
|
|
17
|
+
"""从 background_research.json 收集已知人名(characters 键 + character_details 键及别名)。
|
|
18
|
+
|
|
19
|
+
返回去重后、长度 >=2 的人名列表(按长度降序,长名优先匹配)。文件缺失或无名字时返回 []。
|
|
20
|
+
"""
|
|
21
|
+
research_path = Path(work_dir) / "background_research.json"
|
|
22
|
+
if not research_path.exists():
|
|
23
|
+
return []
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(research_path.read_text(encoding="utf-8"))
|
|
26
|
+
except (OSError, ValueError):
|
|
27
|
+
return []
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
names = set()
|
|
32
|
+
characters = data.get("characters")
|
|
33
|
+
if isinstance(characters, dict):
|
|
34
|
+
names.update(characters.keys())
|
|
35
|
+
details = data.get("character_details")
|
|
36
|
+
if isinstance(details, dict):
|
|
37
|
+
for key, val in details.items():
|
|
38
|
+
names.add(key)
|
|
39
|
+
if isinstance(val, dict):
|
|
40
|
+
aliases = val.get("aliases")
|
|
41
|
+
if isinstance(aliases, list):
|
|
42
|
+
names.update(a for a in aliases if isinstance(a, str))
|
|
43
|
+
|
|
44
|
+
cleaned = {n for n in names if isinstance(n, str) and len(n) >= 2}
|
|
45
|
+
return sorted(cleaned, key=len, reverse=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _correct_text_with_glossary(text, names):
|
|
49
|
+
"""用人名表修正 ASR 同音字错误(如 叶青眉 → 叶轻眉)。
|
|
50
|
+
|
|
51
|
+
对每个已知人名,扫描文本中所有等长窗口;若某窗口与人名恰好相差一个字符(仅一处不同),
|
|
52
|
+
则替换为该人名。严格约束为「恰好一字之差」,避免过度纠正(叶轻风 与 叶轻眉 也是一字之差,
|
|
53
|
+
但这正是限制为单字替换的边界——只有当窗口本身不是任何已知人名时才会被改写)。
|
|
54
|
+
"""
|
|
55
|
+
if not text or not names:
|
|
56
|
+
return text
|
|
57
|
+
name_set = set(names)
|
|
58
|
+
for name in names:
|
|
59
|
+
n = len(name)
|
|
60
|
+
if n < 2 or len(text) < n:
|
|
61
|
+
continue
|
|
62
|
+
i = 0
|
|
63
|
+
while i <= len(text) - n:
|
|
64
|
+
window = text[i:i + n]
|
|
65
|
+
# Only rewrite a one-char-off window when it is NOT itself a known name —
|
|
66
|
+
# otherwise a distinct real name one char away (叶轻风 vs 叶轻眉) would be corrupted.
|
|
67
|
+
if window != name and window not in name_set and _one_char_diff(window, name):
|
|
68
|
+
text = text[:i] + name + text[i + n:]
|
|
69
|
+
i += n
|
|
70
|
+
else:
|
|
71
|
+
i += 1
|
|
72
|
+
return text
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _one_char_diff(a, b):
|
|
76
|
+
"""两个等长字符串是否恰好相差一个字符(仅一处不同)。"""
|
|
77
|
+
if len(a) != len(b):
|
|
78
|
+
return False
|
|
79
|
+
diff = 0
|
|
80
|
+
for ca, cb in zip(a, b):
|
|
81
|
+
if ca != cb:
|
|
82
|
+
diff += 1
|
|
83
|
+
if diff > 1:
|
|
84
|
+
return False
|
|
85
|
+
return diff == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _apply_glossary_corrections(segments, work_dir):
|
|
89
|
+
"""对已转录的 segments 就地应用人名表修正;无人名表时为 no-op。"""
|
|
90
|
+
names = _load_name_glossary(work_dir)
|
|
91
|
+
if not names:
|
|
92
|
+
return segments
|
|
93
|
+
for seg in segments:
|
|
94
|
+
original = seg.get("text") or ""
|
|
95
|
+
corrected = _correct_text_with_glossary(original, names)
|
|
96
|
+
if corrected != original:
|
|
97
|
+
seg["text"] = corrected
|
|
98
|
+
return segments
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _audio_meta_path(work_dir):
|
|
102
|
+
return Path(work_dir) / "audio.wav.meta.json"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _write_audio_meta(work_dir, video_path):
|
|
106
|
+
_audio_meta_path(work_dir).write_text(
|
|
107
|
+
json.dumps({
|
|
108
|
+
"schema_version": 1,
|
|
109
|
+
"source_video_fingerprint": file_fingerprint(video_path),
|
|
110
|
+
"audio": "audio.wav",
|
|
111
|
+
}, ensure_ascii=False, indent=2),
|
|
112
|
+
encoding="utf-8",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def transcribe_audio(video_path, work_dir):
|
|
117
|
+
"""提取音频并用 MiMo ASR 分段转录,通过分段合成时间戳。"""
|
|
118
|
+
asr_file = work_dir / "asr_result.json"
|
|
119
|
+
|
|
120
|
+
if not CONFIG.get("mimo_asr_api_key"):
|
|
121
|
+
key_name = CONFIG.get("mimo_asr_api_key_source", "MIMO_API_KEY")
|
|
122
|
+
log(f"ASR 跳过:未设置 {key_name}(MiMo ASR 需要;VLM/TTS 也需要同一个 key)。"
|
|
123
|
+
f"如不需要对白可加 --skip-asr")
|
|
124
|
+
asr_file.write_text(json.dumps([], ensure_ascii=False, indent=2), encoding="utf-8")
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
# 提取音频
|
|
128
|
+
audio_wav = work_dir / "audio.wav"
|
|
129
|
+
cmd = ["ffmpeg", "-y", "-i", str(video_path), "-vn",
|
|
130
|
+
"-ar", "16000", "-ac", "1", str(audio_wav)]
|
|
131
|
+
result = run_cmd(cmd)
|
|
132
|
+
if result.returncode != 0:
|
|
133
|
+
raise RuntimeError(f"音频提取失败: {result.stderr}")
|
|
134
|
+
_write_audio_meta(work_dir, video_path)
|
|
135
|
+
|
|
136
|
+
# 获取音频时长
|
|
137
|
+
duration = get_video_duration(video_path)
|
|
138
|
+
if duration <= 0:
|
|
139
|
+
# ffprobe 失败时不再伪造 180s 时长,否则会向 asr_result.json 写入虚构时间戳
|
|
140
|
+
log("ASR 警告: 无法获取音频时长(ffprobe 失败),跳过 ASR 转录")
|
|
141
|
+
asr_file.write_text(json.dumps([], ensure_ascii=False, indent=2), encoding="utf-8")
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
segments_dir = work_dir / "audio_segments"
|
|
145
|
+
segments_dir.mkdir(exist_ok=True)
|
|
146
|
+
|
|
147
|
+
segment_length = max(5, int(CONFIG.get("asr_segment_seconds", 30) or 30))
|
|
148
|
+
if duration <= segment_length:
|
|
149
|
+
# 短音频,整段转录
|
|
150
|
+
text = _run_asr(audio_wav)
|
|
151
|
+
asr_result = [{"start": 0.0, "end": round(duration, 2), "text": text}]
|
|
152
|
+
else:
|
|
153
|
+
# 长音频,分段转录(更细的窗口 → 更精细的对白时间戳)
|
|
154
|
+
asr_result = _segment_and_transcribe(audio_wav, segments_dir, duration, segment_length)
|
|
155
|
+
|
|
156
|
+
# 用 background_research.json 的人名表修正 ASR 同音字错误(如 叶青眉 → 叶轻眉);无人名表时为 no-op
|
|
157
|
+
_apply_glossary_corrections(asr_result, work_dir)
|
|
158
|
+
|
|
159
|
+
# 保存
|
|
160
|
+
asr_file.write_text(json.dumps(asr_result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
161
|
+
|
|
162
|
+
total_text = " ".join(s["text"] for s in asr_result if s["text"])
|
|
163
|
+
empty = sum(1 for s in asr_result if not s["text"])
|
|
164
|
+
suffix = f"({empty} 段无文本:静音/切分失败/超限被跳过)" if empty else ""
|
|
165
|
+
log(f"ASR 转录完成: {len(asr_result)} 段, 共 {len(total_text)} 字{suffix}")
|
|
166
|
+
return asr_result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _strip_reasoning_residue(text):
|
|
170
|
+
"""Remove MiMo reasoning-model <think>…</think> leakage from ASR content.
|
|
171
|
+
|
|
172
|
+
Thinking-disable is not applied to -asr models (lib._prepare_api_payload), so the reasoning
|
|
173
|
+
model can leak a <think> block — full, truncated/unclosed, or a leading orphan/residual tag
|
|
174
|
+
(a bare "think>" prefix) — into the transcript. The dubbing implementation carries its own
|
|
175
|
+
copy of this policy; keep behavior aligned through tests.
|
|
176
|
+
"""
|
|
177
|
+
text = re.sub(r"(?is)<think\b.*?</think\s*>", "", text) # full <think>…</think> block
|
|
178
|
+
text = re.sub(r"(?is)<think\b.*\Z", "", text) # unclosed/truncated <think tail
|
|
179
|
+
text = re.sub(r"(?i)^\s*<?/?think\s*>\s*", "", text) # leading orphan/residual think tag
|
|
180
|
+
return text
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _run_asr(wav_path):
|
|
184
|
+
"""用 MiMo ASR (mimo-v2.5-asr) 转录单个 wav 文件,返回纯文本。
|
|
185
|
+
|
|
186
|
+
音频以 base64 data-URI 放进 OpenAI 风格的 chat/completions 消息里,转写文本回到
|
|
187
|
+
choices[0].message.content。API/响应结构失败会抛错,避免把瞬时失败缓存成空转写;
|
|
188
|
+
只有无音频、超体积等确定不可发送的片段返回空串。
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
raw = Path(wav_path).read_bytes()
|
|
192
|
+
except OSError as e:
|
|
193
|
+
log(f"ASR 警告: 无法读取音频 {wav_path}: {e}")
|
|
194
|
+
return ""
|
|
195
|
+
if not raw:
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
b64 = base64.b64encode(raw).decode("ascii")
|
|
199
|
+
max_b64_bytes = int(float(CONFIG.get("mimo_asr_base64_max_mb", 10.0)) * 1024 * 1024)
|
|
200
|
+
if len(b64) > max_b64_bytes:
|
|
201
|
+
log(f"ASR 警告: 分片 base64 体积 {len(b64) / 1024 / 1024:.1f}MB 超过 MiMo 上限 "
|
|
202
|
+
f"{CONFIG.get('mimo_asr_base64_max_mb')}MB,跳过该段;可调小 ASR_SEGMENT_SECONDS")
|
|
203
|
+
return ""
|
|
204
|
+
|
|
205
|
+
payload = {
|
|
206
|
+
"model": CONFIG.get("mimo_asr_model", "mimo-v2.5-asr"),
|
|
207
|
+
"messages": [{
|
|
208
|
+
"role": "user",
|
|
209
|
+
"content": [{
|
|
210
|
+
"type": "input_audio",
|
|
211
|
+
"input_audio": {"data": f"data:{_ASR_AUDIO_MIME};base64,{b64}"},
|
|
212
|
+
}],
|
|
213
|
+
}],
|
|
214
|
+
"asr_options": {"language": CONFIG.get("mimo_asr_language", "auto")},
|
|
215
|
+
}
|
|
216
|
+
try:
|
|
217
|
+
resp = mimo_asr_api_call(payload)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
raise RuntimeError(f"MiMo ASR 调用失败: {e}") from e
|
|
220
|
+
try:
|
|
221
|
+
return _strip_reasoning_residue(str(resp["choices"][0]["message"]["content"] or "")).strip()
|
|
222
|
+
except (KeyError, IndexError, TypeError):
|
|
223
|
+
raise RuntimeError(f"MiMo ASR 返回结构异常: {json.dumps(resp, ensure_ascii=False)[:200]}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _segment_and_transcribe(audio_wav, segments_dir, total_duration, segment_length=None):
|
|
227
|
+
"""分段转录长音频"""
|
|
228
|
+
if segment_length is None:
|
|
229
|
+
segment_length = max(5, int(CONFIG.get("asr_segment_seconds", 30) or 30))
|
|
230
|
+
# 长视频 ASR 是顺序调用;可选节流让调用间隔开,降低踩到集群限流的频率(默认 0=不节流)
|
|
231
|
+
try:
|
|
232
|
+
throttle = max(0.0, float(os.environ.get("ASR_THROTTLE_SECONDS", "0") or 0))
|
|
233
|
+
except ValueError:
|
|
234
|
+
throttle = 0.0
|
|
235
|
+
results = []
|
|
236
|
+
|
|
237
|
+
for i, start in enumerate(range(0, int(total_duration), segment_length)):
|
|
238
|
+
if throttle and i:
|
|
239
|
+
time.sleep(throttle)
|
|
240
|
+
end = min(start + segment_length, total_duration)
|
|
241
|
+
seg_wav = segments_dir / f"seg_{i:03d}.wav"
|
|
242
|
+
|
|
243
|
+
cmd = ["ffmpeg", "-y", "-i", str(audio_wav),
|
|
244
|
+
"-ss", str(start), "-to", str(end),
|
|
245
|
+
"-ar", "16000", "-ac", "1", str(seg_wav)]
|
|
246
|
+
cut = run_cmd(cmd)
|
|
247
|
+
if cut.returncode != 0:
|
|
248
|
+
# 切分失败时不要对磁盘上的陈旧/残缺音频转录,否则会得到错位文本
|
|
249
|
+
log(f" 段 {i+1}: 切分失败,跳过转录 ({cut.stderr.strip()[:200]})")
|
|
250
|
+
text = ""
|
|
251
|
+
else:
|
|
252
|
+
text = _run_asr(seg_wav)
|
|
253
|
+
results.append({
|
|
254
|
+
"start": round(start, 2),
|
|
255
|
+
"end": round(end, 2),
|
|
256
|
+
"text": text,
|
|
257
|
+
})
|
|
258
|
+
log(f" 段 {i+1}: {start:.0f}s-{end:.0f}s => {len(text)} 字")
|
|
259
|
+
|
|
260
|
+
return results
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Public narration-brief API for this self-contained skill."""
|
|
2
|
+
|
|
3
|
+
from agent_brief import build_agent_brief
|
|
4
|
+
from brief_context import assess_understanding_substrate
|
|
5
|
+
from narration_lint import lint_narration, validate_narration_or_raise
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"assess_understanding_substrate",
|
|
9
|
+
"build_agent_brief",
|
|
10
|
+
"lint_narration",
|
|
11
|
+
"validate_narration_or_raise",
|
|
12
|
+
]
|