@oh-story/dsh 0.1.4 → 0.1.6
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 +2707 -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,745 @@
|
|
|
1
|
+
"""Snap clip boundaries to complete speech and clean shot transitions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from lib import log
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _valid_silence_windows(silence_periods):
|
|
15
|
+
"""Return sorted well-formed quiet windows as {start,end} floats."""
|
|
16
|
+
windows = []
|
|
17
|
+
for w in silence_periods or []:
|
|
18
|
+
if not isinstance(w, dict):
|
|
19
|
+
continue
|
|
20
|
+
try:
|
|
21
|
+
start = float(w.get("start"))
|
|
22
|
+
end = float(w.get("end"))
|
|
23
|
+
except (TypeError, ValueError):
|
|
24
|
+
continue
|
|
25
|
+
if end >= start:
|
|
26
|
+
windows.append({"start": start, "end": end})
|
|
27
|
+
return sorted(windows, key=lambda w: (w["start"], w["end"]))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _source_artifact_candidates(
|
|
31
|
+
work_dir, filename, source_id=None, source_work_dir=None
|
|
32
|
+
):
|
|
33
|
+
candidates = []
|
|
34
|
+
if source_work_dir:
|
|
35
|
+
candidates.append(Path(work_dir) / source_work_dir / filename)
|
|
36
|
+
if source_id not in (None, ""):
|
|
37
|
+
candidates.append(Path(work_dir) / "sources" / str(source_id) / filename)
|
|
38
|
+
candidates.append(Path(work_dir) / filename)
|
|
39
|
+
# Preserve precedence while avoiding duplicate reads when layouts resolve to one path.
|
|
40
|
+
return list(dict.fromkeys(candidates))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=None):
|
|
44
|
+
"""Load reliable sentence-end pause windows produced by video-understanding.
|
|
45
|
+
|
|
46
|
+
A sentence anchor's `time` is the acoustic pause end, while `pause_start` is already
|
|
47
|
+
after the final spoken sample. Any cut within that closed interval preserves the sentence.
|
|
48
|
+
Low-confidence anchors are deliberately excluded from the hard-safety path.
|
|
49
|
+
"""
|
|
50
|
+
for path in _source_artifact_candidates(
|
|
51
|
+
work_dir, "speech_boundary_anchors.json", source_id, source_work_dir
|
|
52
|
+
):
|
|
53
|
+
if not path.exists():
|
|
54
|
+
continue
|
|
55
|
+
try:
|
|
56
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
57
|
+
except (OSError, ValueError):
|
|
58
|
+
return []
|
|
59
|
+
anchors = payload.get("sentence_anchors") if isinstance(payload, dict) else None
|
|
60
|
+
windows = []
|
|
61
|
+
for anchor in anchors or []:
|
|
62
|
+
if not isinstance(anchor, dict):
|
|
63
|
+
continue
|
|
64
|
+
confidence = str(anchor.get("confidence") or "").strip().lower()
|
|
65
|
+
if confidence not in {"high", "medium"}:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
end = float(anchor.get("time"))
|
|
69
|
+
start = float(anchor.get("pause_start", end))
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
continue
|
|
72
|
+
start = min(start, end)
|
|
73
|
+
windows.append(
|
|
74
|
+
{
|
|
75
|
+
"start": round(max(0.0, start), 3),
|
|
76
|
+
"end": round(max(0.0, end), 3),
|
|
77
|
+
"kind": "sentence_anchor",
|
|
78
|
+
"confidence": confidence,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
return sorted(windows, key=lambda row: (row["start"], row["end"]))
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
|
|
86
|
+
"""Best-effort ASR speech ownership spans used only to decide whether an unsafe edge blocks."""
|
|
87
|
+
payload = None
|
|
88
|
+
for filename in ("asr_clean.json", "asr_result.json"):
|
|
89
|
+
for path in _source_artifact_candidates(
|
|
90
|
+
work_dir, filename, source_id, source_work_dir
|
|
91
|
+
):
|
|
92
|
+
if not path.exists():
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
96
|
+
except (OSError, ValueError):
|
|
97
|
+
payload = None
|
|
98
|
+
if payload is not None:
|
|
99
|
+
break
|
|
100
|
+
if payload is not None:
|
|
101
|
+
break
|
|
102
|
+
if isinstance(payload, dict):
|
|
103
|
+
payload = payload.get("segments") or payload.get("items") or []
|
|
104
|
+
spans = []
|
|
105
|
+
for row in payload or []:
|
|
106
|
+
if not isinstance(row, dict) or not str(row.get("text") or "").strip():
|
|
107
|
+
continue
|
|
108
|
+
try:
|
|
109
|
+
start, end = float(row.get("start")), float(row.get("end"))
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
continue
|
|
112
|
+
if end > start:
|
|
113
|
+
spans.append({"start": max(0.0, start), "end": end})
|
|
114
|
+
merged = []
|
|
115
|
+
for span in sorted(spans, key=lambda row: (row["start"], row["end"])):
|
|
116
|
+
if merged and span["start"] <= merged[-1]["end"] + 0.05:
|
|
117
|
+
merged[-1]["end"] = max(merged[-1]["end"], span["end"])
|
|
118
|
+
else:
|
|
119
|
+
merged.append(dict(span))
|
|
120
|
+
return merged
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _combine_boundary_windows(*groups):
|
|
124
|
+
rows = []
|
|
125
|
+
for group in groups:
|
|
126
|
+
rows.extend(_valid_silence_windows(group))
|
|
127
|
+
unique = {(round(row["start"], 3), round(row["end"], 3)) for row in rows}
|
|
128
|
+
return [{"start": start, "end": end} for start, end in sorted(unique)]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _same_source(left, right):
|
|
132
|
+
left_id, right_id = left.get("source_id"), right.get("source_id")
|
|
133
|
+
if left_id not in (None, "") or right_id not in (None, ""):
|
|
134
|
+
return (
|
|
135
|
+
left_id not in (None, "")
|
|
136
|
+
and right_id not in (None, "")
|
|
137
|
+
and str(left_id) == str(right_id)
|
|
138
|
+
)
|
|
139
|
+
left_path, right_path = left.get("source_path"), right.get("source_path")
|
|
140
|
+
if left_path or right_path:
|
|
141
|
+
return bool(left_path and right_path and str(left_path) == str(right_path))
|
|
142
|
+
return True # single-source normalized plans intentionally omit source identity
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _continuous_source_join(left, right, tolerance=0.05):
|
|
146
|
+
return bool(
|
|
147
|
+
_same_source(left, right)
|
|
148
|
+
and abs(
|
|
149
|
+
float(left.get("source_end", 0.0)) - float(right.get("source_start", 0.0))
|
|
150
|
+
)
|
|
151
|
+
<= tolerance
|
|
152
|
+
and abs(
|
|
153
|
+
float(left.get("output_end", 0.0)) - float(right.get("output_start", 0.0))
|
|
154
|
+
)
|
|
155
|
+
<= tolerance
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def enforce_clip_sentence_boundaries(
|
|
160
|
+
plan, boundary_windows, speech_spans, video_duration, tolerance=0.05
|
|
161
|
+
):
|
|
162
|
+
"""Block any audible clip edge that falls inside detected source speech.
|
|
163
|
+
|
|
164
|
+
Safe edges are: source start/end, a reliable sentence/quiet pause, or a truly contiguous
|
|
165
|
+
same-source join (no media is removed). Missing ASR timing degrades to `unchecked` rather
|
|
166
|
+
than inventing speech. Once ASR says an edge is speech-owned, failure to snap is blocking.
|
|
167
|
+
"""
|
|
168
|
+
clips = plan.get("clips") or []
|
|
169
|
+
windows = _valid_silence_windows(boundary_windows)
|
|
170
|
+
spans = _valid_silence_windows(speech_spans)
|
|
171
|
+
video_duration = max(0.0, float(video_duration or 0.0))
|
|
172
|
+
tolerance = max(0.001, float(tolerance or 0.05))
|
|
173
|
+
checks, new_blockers = [], []
|
|
174
|
+
|
|
175
|
+
def in_windows(ts):
|
|
176
|
+
return any(
|
|
177
|
+
row["start"] - tolerance <= ts <= row["end"] + tolerance for row in windows
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def in_speech(ts):
|
|
181
|
+
return any(
|
|
182
|
+
row["start"] - tolerance <= ts <= row["end"] + tolerance for row in spans
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
for idx, clip in enumerate(clips):
|
|
186
|
+
for edge, ts in (
|
|
187
|
+
("start", float(clip["source_start"])),
|
|
188
|
+
("end", float(clip["source_end"])),
|
|
189
|
+
):
|
|
190
|
+
contiguous = (
|
|
191
|
+
edge == "start"
|
|
192
|
+
and idx > 0
|
|
193
|
+
and _continuous_source_join(clips[idx - 1], clip, tolerance)
|
|
194
|
+
) or (
|
|
195
|
+
edge == "end"
|
|
196
|
+
and idx + 1 < len(clips)
|
|
197
|
+
and _continuous_source_join(clip, clips[idx + 1], tolerance)
|
|
198
|
+
)
|
|
199
|
+
if edge == "start" and ts <= tolerance:
|
|
200
|
+
status, reason = "safe", "source_start"
|
|
201
|
+
elif edge == "end" and ts >= video_duration - tolerance:
|
|
202
|
+
status, reason = "safe", "source_end"
|
|
203
|
+
elif contiguous:
|
|
204
|
+
status, reason = "safe", "continuous_source_join"
|
|
205
|
+
elif in_windows(ts):
|
|
206
|
+
status, reason = "safe", "sentence_or_quiet_boundary"
|
|
207
|
+
elif not spans:
|
|
208
|
+
status, reason = "unchecked", "speech_timing_unavailable"
|
|
209
|
+
elif not in_speech(ts):
|
|
210
|
+
status, reason = "safe", "outside_detected_speech"
|
|
211
|
+
else:
|
|
212
|
+
status, reason = "blocking", "inside_detected_speech"
|
|
213
|
+
check = {
|
|
214
|
+
"clip_id": clip.get("clip_id", idx),
|
|
215
|
+
"source_id": clip.get("source_id"),
|
|
216
|
+
"edge": edge,
|
|
217
|
+
"time": round(ts, 3),
|
|
218
|
+
"status": status,
|
|
219
|
+
"reason": reason,
|
|
220
|
+
}
|
|
221
|
+
checks.append(check)
|
|
222
|
+
if status == "blocking":
|
|
223
|
+
new_blockers.append(
|
|
224
|
+
{
|
|
225
|
+
"code": "unsafe_clip_sentence_boundary",
|
|
226
|
+
**check,
|
|
227
|
+
"message": "剪辑边界仍落在原声讲话区间内,必须移动到句末锚点,不能截断原声句子。",
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
qc = plan.setdefault("qc", {})
|
|
232
|
+
qc.setdefault("boundary_status", {})["sentence_checks"] = checks
|
|
233
|
+
existing = [
|
|
234
|
+
row
|
|
235
|
+
for row in (qc.get("blocking") or [])
|
|
236
|
+
if not isinstance(row, dict)
|
|
237
|
+
or row.get("code") != "unsafe_clip_sentence_boundary"
|
|
238
|
+
]
|
|
239
|
+
if existing or new_blockers:
|
|
240
|
+
qc["blocking"] = existing + new_blockers
|
|
241
|
+
else:
|
|
242
|
+
qc.pop("blocking", None)
|
|
243
|
+
return plan
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _recompute_clip_timeline(clips):
|
|
247
|
+
cursor = 0.0
|
|
248
|
+
for clip in clips:
|
|
249
|
+
duration = round(float(clip["source_end"]) - float(clip["source_start"]), 3)
|
|
250
|
+
clip["duration"] = duration
|
|
251
|
+
clip["output_start"] = round(cursor, 3)
|
|
252
|
+
clip["output_end"] = round(cursor + duration, 3)
|
|
253
|
+
cursor += duration
|
|
254
|
+
return round(cursor, 3)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _candidate_overlaps(clips, idx, new_start, new_end):
|
|
258
|
+
for j, other in enumerate(clips):
|
|
259
|
+
if j == idx:
|
|
260
|
+
continue
|
|
261
|
+
if new_start < float(other["source_end"]) and new_end > float(
|
|
262
|
+
other["source_start"]
|
|
263
|
+
):
|
|
264
|
+
return True
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def snap_clip_starts_to_lines(
|
|
269
|
+
plan,
|
|
270
|
+
silence_periods,
|
|
271
|
+
video_duration,
|
|
272
|
+
max_prepend,
|
|
273
|
+
max_trim=0.35,
|
|
274
|
+
min_clip_duration=0.3,
|
|
275
|
+
):
|
|
276
|
+
"""Snap clip starts to natural quiet boundaries, preferring safe prepend over trim.
|
|
277
|
+
|
|
278
|
+
Policy:
|
|
279
|
+
- start already inside a quiet window: keep.
|
|
280
|
+
- speech start: prepend to nearest prior quiet window end if within max_prepend.
|
|
281
|
+
- no usable prior quiet: keep and warn, except an extremely near next quiet start
|
|
282
|
+
(<= max_trim) may trim forward if duration/overlap safety holds.
|
|
283
|
+
- never overlap/collapse when allow_overlap is false; unsafe attempts keep-and-warn.
|
|
284
|
+
"""
|
|
285
|
+
silence_periods = _valid_silence_windows(silence_periods)
|
|
286
|
+
if not silence_periods:
|
|
287
|
+
return plan
|
|
288
|
+
|
|
289
|
+
clips = [dict(c) for c in plan["clips"]]
|
|
290
|
+
video_duration = max(0.0, float(video_duration or 0.0))
|
|
291
|
+
max_prepend = max(0.0, float(max_prepend or 0.0))
|
|
292
|
+
max_trim = max(0.0, float(max_trim or 0.0))
|
|
293
|
+
min_duration = max(0.05, float(min_clip_duration or 0.05))
|
|
294
|
+
allow_overlap = bool(plan.get("allow_overlap", False))
|
|
295
|
+
events = []
|
|
296
|
+
|
|
297
|
+
for i, clip in enumerate(clips):
|
|
298
|
+
original_start = float(clip["source_start"])
|
|
299
|
+
source_end = float(clip["source_end"])
|
|
300
|
+
event = {
|
|
301
|
+
"clip_id": clip.get("clip_id", i),
|
|
302
|
+
"source_id": clip.get("source_id"),
|
|
303
|
+
"original_start": round(original_start, 3),
|
|
304
|
+
"action": "kept",
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if any(w["start"] <= original_start <= w["end"] for w in silence_periods):
|
|
308
|
+
event["reason"] = "already_quiet"
|
|
309
|
+
events.append(event)
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
prior_ends = [w["end"] for w in silence_periods if w["end"] <= original_start]
|
|
313
|
+
if prior_ends:
|
|
314
|
+
candidate_start = max(prior_ends)
|
|
315
|
+
if original_start - candidate_start <= max_prepend:
|
|
316
|
+
candidate_start = round(max(0.0, candidate_start), 3)
|
|
317
|
+
safe = candidate_start < source_end - min_duration + 1e-9
|
|
318
|
+
if safe and not allow_overlap:
|
|
319
|
+
safe = not _candidate_overlaps(
|
|
320
|
+
clips, i, candidate_start, source_end
|
|
321
|
+
)
|
|
322
|
+
if safe:
|
|
323
|
+
clip["source_start"] = candidate_start
|
|
324
|
+
event.update(
|
|
325
|
+
{
|
|
326
|
+
"action": "prepended",
|
|
327
|
+
"new_start": candidate_start,
|
|
328
|
+
"delta": round(original_start - candidate_start, 3),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
events.append(event)
|
|
332
|
+
continue
|
|
333
|
+
reason = "overlap_or_collapse"
|
|
334
|
+
else:
|
|
335
|
+
reason = "prior_quiet_too_far"
|
|
336
|
+
else:
|
|
337
|
+
reason = "no_prior_quiet"
|
|
338
|
+
|
|
339
|
+
next_starts = [
|
|
340
|
+
w["start"] for w in silence_periods if w["start"] >= original_start
|
|
341
|
+
]
|
|
342
|
+
if next_starts:
|
|
343
|
+
candidate_start = min(next_starts)
|
|
344
|
+
trim_delta = candidate_start - original_start
|
|
345
|
+
if 0 < trim_delta <= max_trim:
|
|
346
|
+
candidate_start = round(min(video_duration, candidate_start), 3)
|
|
347
|
+
safe = source_end - candidate_start >= min_duration
|
|
348
|
+
if safe and not allow_overlap:
|
|
349
|
+
safe = not _candidate_overlaps(
|
|
350
|
+
clips, i, candidate_start, source_end
|
|
351
|
+
)
|
|
352
|
+
if safe:
|
|
353
|
+
clip["source_start"] = candidate_start
|
|
354
|
+
event.update(
|
|
355
|
+
{
|
|
356
|
+
"action": "trimmed",
|
|
357
|
+
"new_start": candidate_start,
|
|
358
|
+
"delta": round(trim_delta, 3),
|
|
359
|
+
"fallback_from": reason,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
events.append(event)
|
|
363
|
+
continue
|
|
364
|
+
reason = "unsafe_forward_trim"
|
|
365
|
+
|
|
366
|
+
event["start_unsnapped_reason"] = reason
|
|
367
|
+
event["warning_code"] = "clip_start_unsnapped"
|
|
368
|
+
events.append(event)
|
|
369
|
+
|
|
370
|
+
total_duration = _recompute_clip_timeline(clips)
|
|
371
|
+
result = dict(plan)
|
|
372
|
+
result["clips"] = clips
|
|
373
|
+
result["total_duration"] = total_duration
|
|
374
|
+
qc = dict(result.get("qc") or {})
|
|
375
|
+
boundary = dict(qc.get("boundary_status") or {})
|
|
376
|
+
boundary["start_snaps"] = events
|
|
377
|
+
qc["boundary_status"] = boundary
|
|
378
|
+
warnings = list(qc.get("warnings") or [])
|
|
379
|
+
for event in events:
|
|
380
|
+
if event.get("warning_code"):
|
|
381
|
+
warnings.append(
|
|
382
|
+
{
|
|
383
|
+
"code": event["warning_code"],
|
|
384
|
+
"clip_id": event["clip_id"],
|
|
385
|
+
"source_id": event.get("source_id"),
|
|
386
|
+
"start_unsnapped_reason": event.get("start_unsnapped_reason"),
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
if warnings:
|
|
390
|
+
qc["warnings"] = warnings
|
|
391
|
+
result["qc"] = qc
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def snap_clip_ends_to_lines(plan, silence_periods, video_duration, max_extend):
|
|
396
|
+
"""Extend each clip's source_end forward to the next natural pause, preventing mid-sentence cuts.
|
|
397
|
+
|
|
398
|
+
- If silence_periods is empty/None, returns plan unchanged.
|
|
399
|
+
- If a clip's source_end already falls inside a quiet window, no snap is applied.
|
|
400
|
+
- Otherwise, extends to the next quiet window start, capped by max_extend and video_duration.
|
|
401
|
+
- When plan["allow_overlap"] is False, will not extend into another clip's source range.
|
|
402
|
+
- After snapping, recomputes output_start/output_end/duration for all clips cursor-based.
|
|
403
|
+
- Returns the updated plan dict (all other keys preserved).
|
|
404
|
+
"""
|
|
405
|
+
# silence_periods is loaded from disk; tolerate a stale/hand-edited row by keeping only
|
|
406
|
+
# well-formed quiet windows so a single bad entry can't KeyError-abort the whole cut.
|
|
407
|
+
silence_periods = _valid_silence_windows(silence_periods)
|
|
408
|
+
if not silence_periods:
|
|
409
|
+
return plan
|
|
410
|
+
|
|
411
|
+
clips = [dict(c) for c in plan["clips"]]
|
|
412
|
+
video_duration = float(video_duration)
|
|
413
|
+
max_extend = float(max_extend)
|
|
414
|
+
allow_overlap = bool(plan.get("allow_overlap", False))
|
|
415
|
+
events = []
|
|
416
|
+
|
|
417
|
+
for i, clip in enumerate(clips):
|
|
418
|
+
source_end = clip["source_end"]
|
|
419
|
+
event = {
|
|
420
|
+
"clip_id": clip.get("clip_id", i),
|
|
421
|
+
"source_id": clip.get("source_id"),
|
|
422
|
+
"original_end": round(float(source_end), 3),
|
|
423
|
+
"action": "kept",
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# Already inside a quiet window → already at a natural pause.
|
|
427
|
+
if any(w["start"] <= source_end <= w["end"] for w in silence_periods):
|
|
428
|
+
event["reason"] = "already_quiet"
|
|
429
|
+
events.append(event)
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
# Find the next quiet window start at or after source_end.
|
|
433
|
+
candidates = [w["start"] for w in silence_periods if w["start"] >= source_end]
|
|
434
|
+
if not candidates:
|
|
435
|
+
event["end_unsnapped_reason"] = "no_next_quiet"
|
|
436
|
+
events.append(event)
|
|
437
|
+
continue
|
|
438
|
+
next_quiet_start = min(candidates)
|
|
439
|
+
|
|
440
|
+
# Only snap if the next pause is within reach (≤ max_extend away).
|
|
441
|
+
if next_quiet_start > source_end + max_extend:
|
|
442
|
+
event["end_unsnapped_reason"] = "next_quiet_too_far"
|
|
443
|
+
events.append(event)
|
|
444
|
+
continue
|
|
445
|
+
candidate_end = min(next_quiet_start, video_duration)
|
|
446
|
+
if candidate_end <= source_end:
|
|
447
|
+
event["end_unsnapped_reason"] = "non_forward_candidate"
|
|
448
|
+
events.append(event)
|
|
449
|
+
continue
|
|
450
|
+
|
|
451
|
+
# When overlaps are forbidden, cap against every other clip's source range.
|
|
452
|
+
if not allow_overlap:
|
|
453
|
+
other_starts = [
|
|
454
|
+
c["source_start"]
|
|
455
|
+
for j, c in enumerate(clips)
|
|
456
|
+
if j != i and c["source_start"] > source_end
|
|
457
|
+
]
|
|
458
|
+
if other_starts:
|
|
459
|
+
nearest_other_start = min(other_starts)
|
|
460
|
+
candidate_end = min(candidate_end, nearest_other_start)
|
|
461
|
+
if candidate_end <= source_end:
|
|
462
|
+
event["end_unsnapped_reason"] = "overlap_or_collapse"
|
|
463
|
+
events.append(event)
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
clip["source_end"] = round(candidate_end, 3)
|
|
467
|
+
event.update(
|
|
468
|
+
{
|
|
469
|
+
"action": "extended",
|
|
470
|
+
"new_end": clip["source_end"],
|
|
471
|
+
"delta": round(float(clip["source_end"]) - float(source_end), 3),
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
events.append(event)
|
|
475
|
+
|
|
476
|
+
# Recompute output timeline cursor-based (same cursor logic as normalize_clip_plan).
|
|
477
|
+
total_duration = _recompute_clip_timeline(clips)
|
|
478
|
+
|
|
479
|
+
result = dict(plan)
|
|
480
|
+
result["clips"] = clips
|
|
481
|
+
result["total_duration"] = total_duration
|
|
482
|
+
qc = dict(result.get("qc") or {})
|
|
483
|
+
boundary = dict(qc.get("boundary_status") or {})
|
|
484
|
+
boundary["end_snaps"] = events
|
|
485
|
+
qc["boundary_status"] = boundary
|
|
486
|
+
result["qc"] = qc
|
|
487
|
+
return result
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
|
|
491
|
+
"""Absolute source-time hard cuts inside [win_start, win_end] via ffmpeg's scene metric.
|
|
492
|
+
|
|
493
|
+
Input-seek to a little before the window: the rebased output PTS restarts at ~0 at the seek
|
|
494
|
+
target, so `seek + pts_time` recovers absolute source time. The `lead` keeps the seek/keyframe
|
|
495
|
+
settling artifact frame outside [win_start, win_end] so it is filtered out, not mistaken for a
|
|
496
|
+
cut. Returns [] on any ffmpeg trouble (advisory pass must never block the cut).
|
|
497
|
+
"""
|
|
498
|
+
win_start = max(0.0, float(win_start))
|
|
499
|
+
win_end = max(win_start, float(win_end))
|
|
500
|
+
if win_end - win_start < 1e-3:
|
|
501
|
+
return []
|
|
502
|
+
seek = max(0.0, win_start - lead)
|
|
503
|
+
dur = (win_end - seek) + 0.1
|
|
504
|
+
cmd = [
|
|
505
|
+
"ffmpeg",
|
|
506
|
+
"-hide_banner",
|
|
507
|
+
"-nostats",
|
|
508
|
+
"-ss",
|
|
509
|
+
f"{seek:.3f}",
|
|
510
|
+
"-i",
|
|
511
|
+
str(video),
|
|
512
|
+
"-t",
|
|
513
|
+
f"{dur:.3f}",
|
|
514
|
+
"-an",
|
|
515
|
+
"-sn",
|
|
516
|
+
"-filter:v",
|
|
517
|
+
f"select='gt(scene,{threshold})',showinfo",
|
|
518
|
+
"-f",
|
|
519
|
+
"null",
|
|
520
|
+
"-",
|
|
521
|
+
]
|
|
522
|
+
try:
|
|
523
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
524
|
+
except (OSError, ValueError):
|
|
525
|
+
return []
|
|
526
|
+
changes = []
|
|
527
|
+
for m in re.finditer(r"pts_time:([0-9.]+)", proc.stderr or ""):
|
|
528
|
+
t = seek + float(m.group(1))
|
|
529
|
+
if win_start <= t <= win_end:
|
|
530
|
+
changes.append(round(t, 3))
|
|
531
|
+
return sorted(set(changes))
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def snap_clips_off_shot_changes(
|
|
535
|
+
plan, video, video_duration, margin, threshold, min_keep=0.5
|
|
536
|
+
):
|
|
537
|
+
"""Nudge each clip's boundaries clear of the ORIGINAL footage's hard cuts to avoid 闪烁.
|
|
538
|
+
|
|
539
|
+
A clip whose source_start sits just before a shot-change opens on a brief sliver of the old
|
|
540
|
+
shot that then hard-cuts again; one whose source_end sits just after a shot-change closes on a
|
|
541
|
+
sliver of the next shot. Both flash at the edit point. So:
|
|
542
|
+
- move source_start FORWARD onto a shot-change in (start, start+margin] → clean open
|
|
543
|
+
- move source_end BACK onto a shot-change in [end-margin, end) → clean close
|
|
544
|
+
Boundaries already on a cut, or with no nearby cut, are left untouched. Snaps that would shrink
|
|
545
|
+
a clip below `min_keep` are skipped. Recomputes the output timeline cursor-based. Advisory: any
|
|
546
|
+
detection failure leaves that boundary as-is.
|
|
547
|
+
"""
|
|
548
|
+
del video_duration # Positional compatibility for downstream direct callers.
|
|
549
|
+
margin = float(margin)
|
|
550
|
+
if margin <= 0 or not plan.get("clips"):
|
|
551
|
+
return plan
|
|
552
|
+
clips = [dict(c) for c in plan["clips"]]
|
|
553
|
+
n_start = n_end = 0
|
|
554
|
+
events = []
|
|
555
|
+
for i, clip in enumerate(clips):
|
|
556
|
+
s = float(clip["source_start"])
|
|
557
|
+
e = float(clip["source_end"])
|
|
558
|
+
new_s, new_e = s, e
|
|
559
|
+
event = {
|
|
560
|
+
"clip_id": clip.get("clip_id", i),
|
|
561
|
+
"source_id": clip.get("source_id"),
|
|
562
|
+
"original_start": round(s, 3),
|
|
563
|
+
"original_end": round(e, 3),
|
|
564
|
+
"start_action": "kept",
|
|
565
|
+
"end_action": "kept",
|
|
566
|
+
}
|
|
567
|
+
# Opening: a shot-change just AFTER source_start leaves an old-shot sliver before it.
|
|
568
|
+
start_changes = [
|
|
569
|
+
c
|
|
570
|
+
for c in _detect_shot_changes(video, s, min(e, s + margin), threshold)
|
|
571
|
+
if c > s + 1e-3
|
|
572
|
+
]
|
|
573
|
+
if start_changes:
|
|
574
|
+
cand = max(start_changes) # open after the last rapid cut in the window
|
|
575
|
+
if cand < e - min_keep:
|
|
576
|
+
new_s = round(cand, 3)
|
|
577
|
+
event["start_action"] = "moved_forward"
|
|
578
|
+
event["new_start"] = new_s
|
|
579
|
+
else:
|
|
580
|
+
event["start_unsnapped_reason"] = "collapse"
|
|
581
|
+
# Closing: a shot-change just BEFORE source_end leaves a next-shot sliver after it.
|
|
582
|
+
end_changes = [
|
|
583
|
+
c
|
|
584
|
+
for c in _detect_shot_changes(video, max(new_s, e - margin), e, threshold)
|
|
585
|
+
if c < e - 1e-3
|
|
586
|
+
]
|
|
587
|
+
if end_changes:
|
|
588
|
+
cand = min(end_changes) # close before the first rapid cut in the window
|
|
589
|
+
if cand > new_s + min_keep:
|
|
590
|
+
new_e = round(cand, 3)
|
|
591
|
+
event["end_action"] = "moved_back"
|
|
592
|
+
event["new_end"] = new_e
|
|
593
|
+
else:
|
|
594
|
+
event["end_unsnapped_reason"] = "collapse"
|
|
595
|
+
n_start += new_s != s
|
|
596
|
+
n_end += new_e != e
|
|
597
|
+
clip["source_start"] = new_s
|
|
598
|
+
clip["source_end"] = new_e
|
|
599
|
+
events.append(event)
|
|
600
|
+
|
|
601
|
+
# Recompute output timeline cursor-based (same cursor logic as snap_clip_ends_to_lines).
|
|
602
|
+
total_duration = _recompute_clip_timeline(clips)
|
|
603
|
+
|
|
604
|
+
if n_start or n_end:
|
|
605
|
+
log(
|
|
606
|
+
f"避让原片切镜头: {n_start} 个起点前移、{n_end} 个终点回收 (margin={margin}s, 阈值={threshold})"
|
|
607
|
+
)
|
|
608
|
+
result = dict(plan)
|
|
609
|
+
result["clips"] = clips
|
|
610
|
+
result["total_duration"] = total_duration
|
|
611
|
+
qc = dict(result.get("qc") or {})
|
|
612
|
+
boundary = dict(qc.get("boundary_status") or {})
|
|
613
|
+
boundary["shot_snaps"] = events
|
|
614
|
+
qc["boundary_status"] = boundary
|
|
615
|
+
result["qc"] = qc
|
|
616
|
+
return result
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _load_silence_for_source(work_dir, source_id, source_work_dir=None):
|
|
620
|
+
"""Read a source's silence_periods.json from the multi-source work layout (best-effort)."""
|
|
621
|
+
for path in _source_artifact_candidates(
|
|
622
|
+
work_dir, "silence_periods.json", source_id, source_work_dir
|
|
623
|
+
):
|
|
624
|
+
if path.exists():
|
|
625
|
+
try:
|
|
626
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
627
|
+
except (OSError, ValueError):
|
|
628
|
+
return []
|
|
629
|
+
return data if isinstance(data, list) else []
|
|
630
|
+
return []
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def snap_multi_source_clips(
|
|
634
|
+
plan,
|
|
635
|
+
sources,
|
|
636
|
+
work_dir,
|
|
637
|
+
*,
|
|
638
|
+
line_max_extend,
|
|
639
|
+
scene_margin,
|
|
640
|
+
scene_threshold,
|
|
641
|
+
do_line_snap=True,
|
|
642
|
+
do_scene_snap=True,
|
|
643
|
+
start_max_prepend=None,
|
|
644
|
+
start_max_trim=None,
|
|
645
|
+
):
|
|
646
|
+
"""Per-source line/shot snapping for a multi-source validated plan.
|
|
647
|
+
|
|
648
|
+
Each clip is snapped using ITS OWN source's silence windows / shot changes and duration
|
|
649
|
+
(a clip in source B never constrains a clip in source A), then the global OUTPUT timeline
|
|
650
|
+
is recomputed once in plan order. Advisory: missing silence data or ffmpeg trouble leaves a
|
|
651
|
+
boundary unchanged. Mirrors the single-source snap_clip_ends_to_lines + snap_clips_off_shot_changes.
|
|
652
|
+
"""
|
|
653
|
+
clips = plan.get("clips") or []
|
|
654
|
+
if not clips:
|
|
655
|
+
return plan
|
|
656
|
+
allow_overlap = bool(plan.get("allow_overlap", False))
|
|
657
|
+
groups = {}
|
|
658
|
+
for clip in clips:
|
|
659
|
+
groups.setdefault(str(clip.get("source_id")), []).append(clip)
|
|
660
|
+
boundary_accum = {
|
|
661
|
+
"start_snaps": [],
|
|
662
|
+
"end_snaps": [],
|
|
663
|
+
"shot_snaps": [],
|
|
664
|
+
"sentence_checks": [],
|
|
665
|
+
}
|
|
666
|
+
blocking_accum = []
|
|
667
|
+
for sid, group in groups.items():
|
|
668
|
+
source = sources.get(sid, {}) if isinstance(sources, dict) else {}
|
|
669
|
+
duration = float(source.get("duration") or 0.0) or max(
|
|
670
|
+
(float(c["source_end"]) for c in group), default=0.0
|
|
671
|
+
)
|
|
672
|
+
mini = {"clips": [dict(c) for c in group], "allow_overlap": allow_overlap}
|
|
673
|
+
# Visual cleanup goes first. Sentence/quiet snapping is the final authority because
|
|
674
|
+
# a clean picture is never allowed to reintroduce a mid-sentence audio cut.
|
|
675
|
+
if do_scene_snap and source.get("source_path"):
|
|
676
|
+
mini = snap_clips_off_shot_changes(
|
|
677
|
+
mini,
|
|
678
|
+
source["source_path"],
|
|
679
|
+
video_duration=duration,
|
|
680
|
+
margin=scene_margin,
|
|
681
|
+
threshold=scene_threshold,
|
|
682
|
+
)
|
|
683
|
+
source_work_dir = source.get("source_work_dir")
|
|
684
|
+
silence = _load_silence_for_source(work_dir, sid, source_work_dir)
|
|
685
|
+
anchors = _load_sentence_boundary_windows(work_dir, sid, source_work_dir)
|
|
686
|
+
boundaries = _combine_boundary_windows(silence, anchors)
|
|
687
|
+
if do_line_snap:
|
|
688
|
+
if start_max_prepend is not None:
|
|
689
|
+
mini = snap_clip_starts_to_lines(
|
|
690
|
+
mini,
|
|
691
|
+
boundaries,
|
|
692
|
+
duration,
|
|
693
|
+
start_max_prepend,
|
|
694
|
+
max_trim=start_max_trim if start_max_trim is not None else 0.35,
|
|
695
|
+
)
|
|
696
|
+
mini = snap_clip_ends_to_lines(mini, boundaries, duration, line_max_extend)
|
|
697
|
+
mini = enforce_clip_sentence_boundaries(
|
|
698
|
+
mini,
|
|
699
|
+
boundaries,
|
|
700
|
+
_load_source_speech_spans(work_dir, sid, source_work_dir),
|
|
701
|
+
duration,
|
|
702
|
+
)
|
|
703
|
+
mini_boundary = (mini.get("qc") or {}).get("boundary_status") or {}
|
|
704
|
+
for key in boundary_accum:
|
|
705
|
+
boundary_accum[key].extend(
|
|
706
|
+
[e for e in mini_boundary.get(key, []) if isinstance(e, dict)]
|
|
707
|
+
)
|
|
708
|
+
blocking_accum.extend(
|
|
709
|
+
[
|
|
710
|
+
row
|
|
711
|
+
for row in ((mini.get("qc") or {}).get("blocking") or [])
|
|
712
|
+
if isinstance(row, dict)
|
|
713
|
+
]
|
|
714
|
+
)
|
|
715
|
+
for original, snapped in zip(group, mini["clips"]):
|
|
716
|
+
original["source_start"] = snapped["source_start"]
|
|
717
|
+
original["source_end"] = snapped["source_end"]
|
|
718
|
+
# Recompute the global output timeline cursor-based, in plan order (not group order).
|
|
719
|
+
plan["total_duration"] = _recompute_clip_timeline(clips)
|
|
720
|
+
|
|
721
|
+
# Preserve per-source boundary QC as the single validated-plan QC source.
|
|
722
|
+
if any(boundary_accum.values()):
|
|
723
|
+
qc = plan.setdefault("qc", {})
|
|
724
|
+
boundary = qc.setdefault("boundary_status", {})
|
|
725
|
+
for key, events in boundary_accum.items():
|
|
726
|
+
if events:
|
|
727
|
+
boundary.setdefault(key, []).extend(events)
|
|
728
|
+
warnings = qc.setdefault("warnings", [])
|
|
729
|
+
for event in boundary_accum["start_snaps"]:
|
|
730
|
+
if event.get("warning_code"):
|
|
731
|
+
warnings.append(
|
|
732
|
+
{
|
|
733
|
+
"code": event["warning_code"],
|
|
734
|
+
"clip_id": event.get("clip_id"),
|
|
735
|
+
"source_id": event.get("source_id"),
|
|
736
|
+
"start_unsnapped_reason": event.get("start_unsnapped_reason"),
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
# Blocking rows are propagated unconditionally. They used to sit inside the
|
|
740
|
+
# `any(boundary_accum.values())` branch, which happens to hold today only because
|
|
741
|
+
# enforce_clip_sentence_boundaries always emits sentence_checks — a mid-sentence cut
|
|
742
|
+
# must never be silently dropped because some sibling QC list came back empty.
|
|
743
|
+
if blocking_accum:
|
|
744
|
+
plan.setdefault("qc", {}).setdefault("blocking", []).extend(blocking_accum)
|
|
745
|
+
return plan
|