@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,486 @@
|
|
|
1
|
+
"""Original-dialogue subtitle loading, source mapping, and gap placement."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from artifacts import _load_work_json
|
|
8
|
+
from audio_mix import _seg_place_window
|
|
9
|
+
from lib import CONFIG
|
|
10
|
+
from media import _load_cut_timeline_plan
|
|
11
|
+
from assemble_constants import (
|
|
12
|
+
_AUTO_ORIGINAL_READ_CPS,
|
|
13
|
+
_MAX_ORIGINAL_READ_CPS,
|
|
14
|
+
_MIN_ASR_CLIP_OVERLAP,
|
|
15
|
+
_MIN_GAP_TO_SUBTITLE,
|
|
16
|
+
_MIN_READABLE_SECONDS,
|
|
17
|
+
_SUBTITLE_CLOSING_QUOTES,
|
|
18
|
+
)
|
|
19
|
+
from subtitle_core import (
|
|
20
|
+
_bracketed_original_chunks,
|
|
21
|
+
_subtitle_entries,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _has_user_subtitles(work_dir):
|
|
25
|
+
"""True when the user dropped a bring-your-own original-subtitle file into work_dir."""
|
|
26
|
+
return work_dir is not None and any(
|
|
27
|
+
(Path(work_dir) / name).exists()
|
|
28
|
+
for name in ("user_subtitles.json", "user_subtitles.srt", "user_subtitles.ass")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _source_subtitle_mask_policy(work_dir=None):
|
|
33
|
+
"""Explicit source-subtitle mask policy and trigger facts for visual QC/cache keys.
|
|
34
|
+
|
|
35
|
+
Older builds treated ``MASK_SOURCE_SUBTITLES=True`` as an ambient default black
|
|
36
|
+
band. The visual contract now requires an explicit policy, so a bare truthy
|
|
37
|
+
legacy flag is represented as ``legacy_implicit`` and blocks the visual gate
|
|
38
|
+
instead of silently masking picture information.
|
|
39
|
+
"""
|
|
40
|
+
burn = bool(CONFIG.get("burn_subtitles", False))
|
|
41
|
+
raw_policy = str(CONFIG.get("source_subtitle_mask_policy", "") or "").strip().lower()
|
|
42
|
+
legacy_flag = bool(CONFIG.get("mask_source_subtitles", False))
|
|
43
|
+
allowed = {"off", "opt_in", "safe", "forced"}
|
|
44
|
+
declared = bool(CONFIG.get("source_subtitle_mask_policy_declared", False)) or raw_policy in {"opt_in", "safe", "forced"}
|
|
45
|
+
implicit = False
|
|
46
|
+
if legacy_flag and not declared:
|
|
47
|
+
raw_policy = "legacy_implicit"
|
|
48
|
+
implicit = True
|
|
49
|
+
elif not raw_policy:
|
|
50
|
+
raw_policy = "off"
|
|
51
|
+
elif raw_policy not in allowed:
|
|
52
|
+
implicit = True
|
|
53
|
+
user_subtitles = _has_user_subtitles(work_dir)
|
|
54
|
+
active = False
|
|
55
|
+
trigger = "policy_off"
|
|
56
|
+
reason = "source subtitle masking disabled by explicit policy"
|
|
57
|
+
if raw_policy == "off":
|
|
58
|
+
active = False
|
|
59
|
+
elif raw_policy in {"opt_in", "forced"}:
|
|
60
|
+
active = burn and legacy_flag
|
|
61
|
+
trigger = "burn_subtitles_and_legacy_mask_flag"
|
|
62
|
+
reason = "explicit policy permits masking only with burned recap subtitles"
|
|
63
|
+
elif raw_policy == "safe":
|
|
64
|
+
active = burn and (legacy_flag or user_subtitles)
|
|
65
|
+
trigger = "safe_policy_with_burned_subtitles"
|
|
66
|
+
reason = "safe policy masks only when recap subtitles are burned and an original-subtitle source is declared"
|
|
67
|
+
else:
|
|
68
|
+
active = False
|
|
69
|
+
trigger = "implicit_or_invalid_policy"
|
|
70
|
+
reason = "mask_source_subtitles requires explicit SOURCE_SUBTITLE_MASK_POLICY"
|
|
71
|
+
if not burn and active:
|
|
72
|
+
active = False
|
|
73
|
+
trigger = "burn_subtitles_disabled"
|
|
74
|
+
reason = "mask-only black band is forbidden without burned recap subtitles"
|
|
75
|
+
return {
|
|
76
|
+
"policy": raw_policy,
|
|
77
|
+
"declared": bool(declared and raw_policy in allowed),
|
|
78
|
+
"active": bool(active),
|
|
79
|
+
"scope": (
|
|
80
|
+
"measured_source_subtitle_band"
|
|
81
|
+
if active and 0 <= int(CONFIG.get("subtitle_y_top", -1)) < int(CONFIG.get("subtitle_y_bot", -1))
|
|
82
|
+
else ("bottom_source_subtitle_band" if active else "none")
|
|
83
|
+
),
|
|
84
|
+
"trigger": trigger,
|
|
85
|
+
"reason": reason,
|
|
86
|
+
"burn_subtitles": burn,
|
|
87
|
+
"legacy_mask_flag": legacy_flag,
|
|
88
|
+
"user_subtitles_present": user_subtitles,
|
|
89
|
+
"blocking": bool(implicit),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _load_original_asr(work_dir):
|
|
94
|
+
"""The original speech transcription (asr_result.json), SOURCE-time, cleaned to
|
|
95
|
+
{start,end,text} with text and a positive span. [] when absent/unparseable."""
|
|
96
|
+
segs = []
|
|
97
|
+
for s in _load_work_json(work_dir, "asr_result.json") or []:
|
|
98
|
+
if not isinstance(s, dict):
|
|
99
|
+
continue
|
|
100
|
+
try:
|
|
101
|
+
start, end = float(s["start"]), float(s["end"])
|
|
102
|
+
except (KeyError, TypeError, ValueError):
|
|
103
|
+
continue
|
|
104
|
+
text = str(s.get("text", "")).strip()
|
|
105
|
+
if text and end > start:
|
|
106
|
+
segs.append({"start": start, "end": end, "text": text})
|
|
107
|
+
return segs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _load_agent_original_subtitles(work_dir):
|
|
111
|
+
"""Agent-calibrated original-dialogue subtitles (original_subtitles.json): OUTPUT-time
|
|
112
|
+
[{start,end,text}] the writer authors alongside narration.json — the corrected, gap-aligned
|
|
113
|
+
transcript of what is ACTUALLY said in each original-audio gap (ASR errors/names fixed).
|
|
114
|
+
None when absent/invalid (then assemble falls back to a conservative auto-ASR mapping)."""
|
|
115
|
+
data = _load_work_json(work_dir, "original_subtitles.json")
|
|
116
|
+
if not isinstance(data, list):
|
|
117
|
+
return None
|
|
118
|
+
out = []
|
|
119
|
+
for s in data:
|
|
120
|
+
if not isinstance(s, dict):
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
start, end = float(s["start"]), float(s["end"])
|
|
124
|
+
except (KeyError, TypeError, ValueError):
|
|
125
|
+
continue
|
|
126
|
+
text = str(s.get("text", "")).strip()
|
|
127
|
+
if text and end > start:
|
|
128
|
+
out.append({"start": start, "end": end, "text": text})
|
|
129
|
+
return out or None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _clean_subtitle_segments(raw):
|
|
133
|
+
"""Coerce an iterable of {start,end,text} dicts to validated, positive-span segments."""
|
|
134
|
+
out = []
|
|
135
|
+
for s in raw or []:
|
|
136
|
+
if not isinstance(s, dict):
|
|
137
|
+
continue
|
|
138
|
+
try:
|
|
139
|
+
start, end = float(s["start"]), float(s["end"])
|
|
140
|
+
except (KeyError, TypeError, ValueError):
|
|
141
|
+
continue
|
|
142
|
+
text = str(s.get("text", "")).strip()
|
|
143
|
+
if text and end > start:
|
|
144
|
+
out.append({"start": start, "end": end, "text": text})
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _parse_srt_timestamp(value):
|
|
149
|
+
"""Parse an SRT 'HH:MM:SS,mmm' (or ASS 'H:MM:SS.cc') timestamp into seconds, or None."""
|
|
150
|
+
m = re.match(r"\s*(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})\s*$", str(value))
|
|
151
|
+
if not m:
|
|
152
|
+
return None
|
|
153
|
+
h, mm, ss, frac = m.groups()
|
|
154
|
+
return int(h) * 3600 + int(mm) * 60 + int(ss) + int(frac) / (10 ** len(frac))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _parse_srt_text(text):
|
|
158
|
+
"""Minimal SRT parser → [{start,end,text}]. Tolerant of blank lines / missing indices."""
|
|
159
|
+
segs = []
|
|
160
|
+
for block in re.split(r"\n\s*\n", str(text).replace("\r\n", "\n").replace("\r", "\n")):
|
|
161
|
+
lines = [ln for ln in block.split("\n") if ln.strip()]
|
|
162
|
+
if not lines:
|
|
163
|
+
continue
|
|
164
|
+
if lines[0].strip().isdigit():
|
|
165
|
+
lines = lines[1:]
|
|
166
|
+
if not lines or "-->" not in lines[0]:
|
|
167
|
+
continue
|
|
168
|
+
parts = lines[0].split("-->")
|
|
169
|
+
if len(parts) != 2:
|
|
170
|
+
continue
|
|
171
|
+
start, end = _parse_srt_timestamp(parts[0]), _parse_srt_timestamp(parts[1])
|
|
172
|
+
body = " ".join(lines[1:]).strip()
|
|
173
|
+
if start is not None and end is not None and end > start and body:
|
|
174
|
+
segs.append({"start": start, "end": end, "text": body})
|
|
175
|
+
return segs
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_ass_text(text):
|
|
179
|
+
"""Minimal ASS Dialogue parser → [{start,end,text}] (Start, End are fields 2 and 3)."""
|
|
180
|
+
segs = []
|
|
181
|
+
for line in str(text).replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
|
182
|
+
if not line.startswith("Dialogue:"):
|
|
183
|
+
continue
|
|
184
|
+
fields = line[len("Dialogue:"):].split(",", 9)
|
|
185
|
+
if len(fields) < 10:
|
|
186
|
+
continue
|
|
187
|
+
start, end = _parse_srt_timestamp(fields[1]), _parse_srt_timestamp(fields[2])
|
|
188
|
+
body = re.sub(r"\{[^}]*\}", "", fields[9]).replace("\\N", " ").replace("\\n", " ").strip()
|
|
189
|
+
if start is not None and end is not None and end > start and body:
|
|
190
|
+
segs.append({"start": start, "end": end, "text": body})
|
|
191
|
+
return segs
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _load_user_original_subtitles(work_dir):
|
|
195
|
+
"""User-supplied original-dialogue subtitles, the highest-priority source (above the agent file).
|
|
196
|
+
|
|
197
|
+
Accepts (first existing wins):
|
|
198
|
+
- user_subtitles.json: a bare list [{start,end,text}] (treated as OUTPUT-time, used verbatim),
|
|
199
|
+
OR a wrapper {"timeline":"source"|"output", "lines":[...]} — "source" is remapped to OUTPUT
|
|
200
|
+
via the cut clip spans, "output" (default) is used directly.
|
|
201
|
+
- user_subtitles.srt / user_subtitles.ass: parsed minimally and defaulted to SOURCE-time,
|
|
202
|
+
so they are remapped to OUTPUT via the cut clip spans.
|
|
203
|
+
Returns OUTPUT-time [{start,end,text}], or None when absent/malformed (caller falls back)."""
|
|
204
|
+
work = Path(work_dir)
|
|
205
|
+
json_path = work / "user_subtitles.json"
|
|
206
|
+
if json_path.exists():
|
|
207
|
+
try:
|
|
208
|
+
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
209
|
+
except (ValueError, OSError):
|
|
210
|
+
return None
|
|
211
|
+
if isinstance(data, list):
|
|
212
|
+
segs, timeline = _clean_subtitle_segments(data), "output"
|
|
213
|
+
elif isinstance(data, dict):
|
|
214
|
+
segs = _clean_subtitle_segments(data.get("lines"))
|
|
215
|
+
timeline = str(data.get("timeline", "output")).lower()
|
|
216
|
+
else:
|
|
217
|
+
return None
|
|
218
|
+
if not segs:
|
|
219
|
+
return None
|
|
220
|
+
if timeline == "source":
|
|
221
|
+
segs = _map_asr_to_output(segs, _output_clip_spans(work))
|
|
222
|
+
return segs or None
|
|
223
|
+
|
|
224
|
+
for name in ("user_subtitles.srt", "user_subtitles.ass"):
|
|
225
|
+
path = work / name
|
|
226
|
+
if not path.exists():
|
|
227
|
+
continue
|
|
228
|
+
try:
|
|
229
|
+
text = path.read_text(encoding="utf-8")
|
|
230
|
+
except OSError:
|
|
231
|
+
return None
|
|
232
|
+
parser = _parse_ass_text if name.endswith(".ass") else _parse_srt_text
|
|
233
|
+
segs = _clean_subtitle_segments(parser(text))
|
|
234
|
+
if not segs:
|
|
235
|
+
return None
|
|
236
|
+
# .srt/.ass default to SOURCE-time → remap onto the output timeline (identity in full mode).
|
|
237
|
+
segs = _map_asr_to_output(segs, _output_clip_spans(work))
|
|
238
|
+
return segs or None
|
|
239
|
+
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _output_clip_spans(work_dir):
|
|
244
|
+
"""Cut-mode source→output clip spans using the same freshness logic as video clips."""
|
|
245
|
+
try:
|
|
246
|
+
plan = _load_cut_timeline_plan(work_dir)
|
|
247
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
248
|
+
plan = None
|
|
249
|
+
if not plan:
|
|
250
|
+
return None
|
|
251
|
+
entries = plan.get("clips", plan) if isinstance(plan, dict) else plan
|
|
252
|
+
if not isinstance(entries, list):
|
|
253
|
+
return None
|
|
254
|
+
spans, cursor = [], 0.0
|
|
255
|
+
for c in entries:
|
|
256
|
+
if not isinstance(c, dict):
|
|
257
|
+
continue
|
|
258
|
+
try:
|
|
259
|
+
ss = float(c.get("source_start", c.get("start")))
|
|
260
|
+
se = float(c.get("source_end", c.get("end")))
|
|
261
|
+
except (TypeError, ValueError):
|
|
262
|
+
continue
|
|
263
|
+
if se - ss <= 0:
|
|
264
|
+
continue
|
|
265
|
+
out_s, out_e = c.get("output_start"), c.get("output_end")
|
|
266
|
+
if out_s is None or out_e is None:
|
|
267
|
+
out_s, out_e = cursor, cursor + (se - ss)
|
|
268
|
+
cursor += se - ss
|
|
269
|
+
else:
|
|
270
|
+
out_s, out_e = float(out_s), float(out_e)
|
|
271
|
+
cursor = max(cursor, out_e)
|
|
272
|
+
spans.append({"source_start": ss, "source_end": se, "output_start": out_s, "output_end": out_e})
|
|
273
|
+
return spans or None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _map_asr_to_output(asr_segs, clip_spans):
|
|
277
|
+
"""Map SOURCE-time ASR segments onto the OUTPUT timeline. Full mode (clip_spans None) is
|
|
278
|
+
identity; cut mode intersects each ASR span with each kept clip (a straddling line yields one
|
|
279
|
+
fragment per clip; lines in cut-away footage are dropped)."""
|
|
280
|
+
if clip_spans is None:
|
|
281
|
+
return [dict(s) for s in asr_segs]
|
|
282
|
+
out = []
|
|
283
|
+
for seg in asr_segs:
|
|
284
|
+
for c in clip_spans:
|
|
285
|
+
ov_s, ov_e = max(seg["start"], c["source_start"]), min(seg["end"], c["source_end"])
|
|
286
|
+
if ov_e - ov_s <= _MIN_ASR_CLIP_OVERLAP:
|
|
287
|
+
continue
|
|
288
|
+
out.append({
|
|
289
|
+
"start": c["output_start"] + (ov_s - c["source_start"]),
|
|
290
|
+
"end": c["output_start"] + (ov_e - c["source_start"]),
|
|
291
|
+
"text": seg["text"],
|
|
292
|
+
})
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _narration_gap_windows(tts_segments, video_duration, min_gap=_MIN_GAP_TO_SUBTITLE):
|
|
297
|
+
"""OUTPUT-timeline stretches with NO narration (the original-audio blocks): the complement of
|
|
298
|
+
the merged narration placement windows within [0, video_duration], keeping gaps >= min_gap."""
|
|
299
|
+
placed = sorted(
|
|
300
|
+
(_seg_place_window(s) for s in tts_segments if isinstance(s, dict)), key=lambda w: w[0])
|
|
301
|
+
merged = []
|
|
302
|
+
for s, e in placed:
|
|
303
|
+
if e - s <= 0:
|
|
304
|
+
continue
|
|
305
|
+
if merged and s <= merged[-1][1]:
|
|
306
|
+
merged[-1][1] = max(merged[-1][1], e)
|
|
307
|
+
else:
|
|
308
|
+
merged.append([s, e])
|
|
309
|
+
gaps, cursor = [], 0.0
|
|
310
|
+
for s, e in merged:
|
|
311
|
+
if s - cursor >= min_gap:
|
|
312
|
+
gaps.append((cursor, s))
|
|
313
|
+
cursor = max(cursor, e)
|
|
314
|
+
if video_duration - cursor >= min_gap:
|
|
315
|
+
gaps.append((cursor, float(video_duration)))
|
|
316
|
+
return gaps
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _original_gap_subtitle_entries(tts_segments, work_dir, video_duration):
|
|
320
|
+
"""Subtitle entries for the ORIGINAL dialogue during the original-audio blocks (narration
|
|
321
|
+
gaps), so the band is not blank while the original speaks. Off unless we are burning and
|
|
322
|
+
subtitle_original_in_gaps is set; no-op when there is no ASR. Cut mode remaps ASR to output."""
|
|
323
|
+
# Fill the gaps when either (a) we are masking the source's own burned-in subs (so the band is
|
|
324
|
+
# blank without us), or (b) the user supplied their own subtitle file — a clear signal they want
|
|
325
|
+
# the original dialogue shown, e.g. a clean/foreign source with mask OFF (no burned subs to
|
|
326
|
+
# double). Without a user file we keep the mask requirement so we don't double the source's own
|
|
327
|
+
# visible subs. subtitle_original_in_gaps is the explicit override either way.
|
|
328
|
+
mask_covers_gaps = _source_subtitle_mask_covers_gaps(work_dir)
|
|
329
|
+
if not (CONFIG.get("burn_subtitles", False)
|
|
330
|
+
and CONFIG.get("subtitle_original_in_gaps", True)
|
|
331
|
+
and (mask_covers_gaps or _has_user_subtitles(work_dir))):
|
|
332
|
+
return []
|
|
333
|
+
gaps = _narration_gap_windows(tts_segments, video_duration)
|
|
334
|
+
if not gaps:
|
|
335
|
+
return []
|
|
336
|
+
|
|
337
|
+
# Source ladder (highest priority first): user-supplied file → agent-calibrated transcript →
|
|
338
|
+
# conservative auto-ASR mapping. The user file and the agent file are time-precise (their spans
|
|
339
|
+
# are the real on-screen windows), so they take the interval-clip "precise" path; raw ASR is
|
|
340
|
+
# coarse and stays on the midpoint+over-render-guard fallback path.
|
|
341
|
+
user = _load_user_original_subtitles(work_dir)
|
|
342
|
+
if user is not None:
|
|
343
|
+
candidates, precise = user, True
|
|
344
|
+
else:
|
|
345
|
+
agent = _load_agent_original_subtitles(work_dir)
|
|
346
|
+
if agent is not None:
|
|
347
|
+
candidates, precise = agent, True
|
|
348
|
+
else:
|
|
349
|
+
asr = _load_original_asr(work_dir)
|
|
350
|
+
if not asr:
|
|
351
|
+
return []
|
|
352
|
+
candidates, precise = _map_asr_to_output(asr, _output_clip_spans(work_dir)), False
|
|
353
|
+
|
|
354
|
+
max_chars = int(CONFIG.get("subtitle_max_chars", 20))
|
|
355
|
+
if precise:
|
|
356
|
+
return _precise_gap_entries(candidates, gaps, max_chars)
|
|
357
|
+
return _fallback_gap_entries(candidates, gaps, max_chars)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _precise_gap_entries(candidates, gaps, max_chars):
|
|
361
|
+
"""Precise path for time-accurate sources (user / agent-calibrated): interval-CLIP each line
|
|
362
|
+
across the gap boundaries it overlaps, emitting one sub-entry per overlapped gap (clipped to
|
|
363
|
+
that gap). A line straddling two gaps is split, not snapped to one or dropped; only sub-fragments
|
|
364
|
+
shorter than _MIN_READABLE_SECONDS are dropped. No over-render guard (the source is trusted)."""
|
|
365
|
+
entries = []
|
|
366
|
+
for seg in candidates:
|
|
367
|
+
text = str(seg["text"]).strip()
|
|
368
|
+
if not text:
|
|
369
|
+
continue
|
|
370
|
+
seg_start, seg_end = float(seg["start"]), float(seg["end"])
|
|
371
|
+
seg_dur = seg_end - seg_start
|
|
372
|
+
overlaps = []
|
|
373
|
+
for gs, ge in gaps:
|
|
374
|
+
cs, ce = max(seg_start, gs), min(seg_end, ge)
|
|
375
|
+
if ce - cs >= _MIN_READABLE_SECONDS:
|
|
376
|
+
overlaps.append((cs, ce))
|
|
377
|
+
if not overlaps:
|
|
378
|
+
continue
|
|
379
|
+
if len(overlaps) == 1 or seg_dur <= 0:
|
|
380
|
+
# the common case (a line authored within one gap): show it whole in that gap
|
|
381
|
+
cs, ce = overlaps[0]
|
|
382
|
+
entries.extend(_bracketed_original_chunks(text, cs, ce, max_chars))
|
|
383
|
+
continue
|
|
384
|
+
# the line straddles a narration block: show each gap only ITS portion of the text
|
|
385
|
+
# (proportional to the time the line overlaps that gap) instead of the whole line twice.
|
|
386
|
+
n = len(text)
|
|
387
|
+
for cs, ce in overlaps:
|
|
388
|
+
lo = max(0, int(round((cs - seg_start) / seg_dur * n)))
|
|
389
|
+
hi = min(n, int(round((ce - seg_start) / seg_dur * n)))
|
|
390
|
+
piece = text[lo:hi].strip()
|
|
391
|
+
if piece:
|
|
392
|
+
entries.extend(_bracketed_original_chunks(piece, cs, ce, max_chars))
|
|
393
|
+
return entries
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _split_sentences_keep_delims(text):
|
|
397
|
+
"""Split on terminal CJK sentence marks 。!? keeping each delimiter with its sentence. A
|
|
398
|
+
fragment that is only closing quotes/brackets (e.g. a trailing 」 after a 。 inside a quote) is
|
|
399
|
+
re-attached to the previous sentence so quoted speech is never split off into a bare 」."""
|
|
400
|
+
parts = [p.strip() for p in re.split(r"(?<=[。!?])", str(text)) if p.strip()]
|
|
401
|
+
merged = []
|
|
402
|
+
for part in parts:
|
|
403
|
+
if merged and all(ch in _SUBTITLE_CLOSING_QUOTES for ch in part):
|
|
404
|
+
merged[-1] += part
|
|
405
|
+
else:
|
|
406
|
+
merged.append(part)
|
|
407
|
+
return merged
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _fallback_gap_entries(candidates, gaps, max_chars):
|
|
411
|
+
"""Coarse-ASR fallback. Each coarse-ASR line spans a whole window with no per-sentence onset, so
|
|
412
|
+
it is split into WHOLE sentences (never mid-word); each sentence is assigned to the gap its
|
|
413
|
+
char-proportional midpoint lands in, and within a gap the assigned sentences are packed
|
|
414
|
+
SEQUENTIALLY from the first one's estimated onset at a comfortable read rate — so two lines in
|
|
415
|
+
one gap never overlap or scatter to char-proportional tail slots — capped at the gap end. An
|
|
416
|
+
over-dense gap front-truncates (shown) rather than dropping to blank."""
|
|
417
|
+
# 1) split each coarse line into WHOLE sentences (never mid-word) and assign each to the gap
|
|
418
|
+
# its char-proportional midpoint lands in (the only "which gap" signal coarse ASR gives).
|
|
419
|
+
buckets = {} # gap_index -> [(estimated_onset, sentence_text)]
|
|
420
|
+
for seg in candidates:
|
|
421
|
+
for sentence in _split_sentences_keep_delims(seg["text"]) or [str(seg["text"]).strip()]:
|
|
422
|
+
text = sentence.strip()
|
|
423
|
+
if not text:
|
|
424
|
+
continue
|
|
425
|
+
sub = _sentence_subspan(seg, sentence)
|
|
426
|
+
mid = (sub["start"] + sub["end"]) / 2.0
|
|
427
|
+
gi = next((i for i, (gs, ge) in enumerate(gaps) if gs <= mid < ge), None)
|
|
428
|
+
if gi is None:
|
|
429
|
+
continue
|
|
430
|
+
buckets.setdefault(gi, []).append((sub["start"], text))
|
|
431
|
+
# 2) within each gap, pack the assigned sentences SEQUENTIALLY from the gap onset at a
|
|
432
|
+
# comfortable read rate. Anchoring to the gap onset (vs each sentence's char-proportional
|
|
433
|
+
# tail position) stops a line heard early from being shoved to the END of its window — the
|
|
434
|
+
# coarse-ASR lag. Full mode keeps the real ASR onset (the first sentence's own start); an
|
|
435
|
+
# over-dense gap front-truncates (shown) rather than dropping to blank.
|
|
436
|
+
entries = []
|
|
437
|
+
for gi, items in buckets.items():
|
|
438
|
+
gs, ge = gaps[gi]
|
|
439
|
+
items.sort(key=lambda it: it[0])
|
|
440
|
+
# start at the first assigned sentence's estimated onset (clamped into the gap), then pack
|
|
441
|
+
# the rest sequentially so they never overlap or scatter to char-proportional tail slots.
|
|
442
|
+
cursor = min(ge, max(gs, min(start for start, _ in items)))
|
|
443
|
+
for _, text in items:
|
|
444
|
+
if cursor >= ge - _MIN_READABLE_SECONDS:
|
|
445
|
+
break
|
|
446
|
+
ce2 = min(ge, cursor + max(_MIN_READABLE_SECONDS, len(text) / _AUTO_ORIGINAL_READ_CPS))
|
|
447
|
+
if ce2 - cursor < _MIN_READABLE_SECONDS:
|
|
448
|
+
break
|
|
449
|
+
max_len = int((ce2 - cursor) * _MAX_ORIGINAL_READ_CPS)
|
|
450
|
+
shown = text if len(text) <= max_len else text[:max_len]
|
|
451
|
+
entries.extend(_bracketed_original_chunks(shown, cursor, ce2, max_chars))
|
|
452
|
+
cursor = ce2
|
|
453
|
+
return entries
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _sentence_subspan(seg, sentence):
|
|
457
|
+
"""The slice of seg's [start,end] window that this sentence occupies, by character proportion.
|
|
458
|
+
Single-sentence lines return the whole span unchanged."""
|
|
459
|
+
full = str(seg["text"]).strip()
|
|
460
|
+
if not full or sentence.strip() == full:
|
|
461
|
+
return {"start": seg["start"], "end": seg["end"]}
|
|
462
|
+
idx = full.find(sentence.strip())
|
|
463
|
+
if idx < 0:
|
|
464
|
+
return {"start": seg["start"], "end": seg["end"]}
|
|
465
|
+
span = seg["end"] - seg["start"]
|
|
466
|
+
s = seg["start"] + span * (idx / len(full))
|
|
467
|
+
e = seg["start"] + span * ((idx + len(sentence.strip())) / len(full))
|
|
468
|
+
return {"start": s, "end": e}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _combined_subtitle_entries(narration, work_dir, video_duration):
|
|
472
|
+
"""Narration subtitle entries plus original-dialogue entries in the gaps, sorted by start.
|
|
473
|
+
Original entries are confined to narration gaps, so they never overlap narration entries."""
|
|
474
|
+
entries = list(_subtitle_entries(narration))
|
|
475
|
+
entries.extend(_original_gap_subtitle_entries(narration, work_dir, video_duration))
|
|
476
|
+
entries.sort(key=lambda x: (x["start"], x["end"]))
|
|
477
|
+
return entries
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _source_subtitle_mask_covers_gaps(work_dir=None):
|
|
481
|
+
"""Whether the effective source mask hides hardcoded subtitles outside narration."""
|
|
482
|
+
if not _source_subtitle_mask_policy(work_dir).get("active"):
|
|
483
|
+
return False
|
|
484
|
+
opacity = max(0.0, min(1.0, float(CONFIG.get("subtitle_mask_opacity", 0.6))))
|
|
485
|
+
timing = str(CONFIG.get("source_subtitle_mask_timing", "narration") or "narration").lower()
|
|
486
|
+
return opacity >= 1.0 - 1e-9 and timing == "all"
|