@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,545 @@
|
|
|
1
|
+
"""Normalize cut plans and maintain render-cache provenance."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from lib import CONFIG, get_video_duration, log
|
|
14
|
+
|
|
15
|
+
EDITED_SOURCE_RENDER_ALGORITHM_VERSION = "edited-source-render-v3"
|
|
16
|
+
|
|
17
|
+
GEOMETRY_RENDER_ALGORITHM_VERSION = "geometry-weighted-orientation-area-fps-v2"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_duration_seconds(value):
|
|
21
|
+
"""Parse seconds, 10m/1h forms, or HH:MM:SS into seconds."""
|
|
22
|
+
if value is None or value == "":
|
|
23
|
+
return None
|
|
24
|
+
if isinstance(value, (int, float)):
|
|
25
|
+
seconds = float(value)
|
|
26
|
+
if seconds <= 0:
|
|
27
|
+
raise ValueError("duration must be positive")
|
|
28
|
+
return seconds
|
|
29
|
+
|
|
30
|
+
text = str(value).strip().lower()
|
|
31
|
+
if not text:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
if ":" in text:
|
|
35
|
+
parts = text.split(":")
|
|
36
|
+
if len(parts) not in (2, 3):
|
|
37
|
+
raise ValueError(f"invalid duration: {value}")
|
|
38
|
+
try:
|
|
39
|
+
nums = [float(p) for p in parts]
|
|
40
|
+
except ValueError as exc:
|
|
41
|
+
raise ValueError(f"invalid duration: {value}") from exc
|
|
42
|
+
if any(n < 0 for n in nums):
|
|
43
|
+
raise ValueError("duration must be positive")
|
|
44
|
+
if nums[-1] >= 60 or (len(nums) == 3 and nums[-2] >= 60):
|
|
45
|
+
raise ValueError(f"invalid duration: {value}")
|
|
46
|
+
if len(nums) == 2:
|
|
47
|
+
seconds = nums[0] * 60 + nums[1]
|
|
48
|
+
else:
|
|
49
|
+
seconds = nums[0] * 3600 + nums[1] * 60 + nums[2]
|
|
50
|
+
if seconds <= 0:
|
|
51
|
+
raise ValueError("duration must be positive")
|
|
52
|
+
return seconds
|
|
53
|
+
|
|
54
|
+
# One or more <number><unit> tokens: "600", "10m", "500ms", "2m30s", "1h5m30s".
|
|
55
|
+
# A bare number is read as seconds; units may be combined (compound durations).
|
|
56
|
+
factors = {"ms": 0.001, "s": 1, "m": 60, "h": 3600}
|
|
57
|
+
sign = 1.0
|
|
58
|
+
body = text
|
|
59
|
+
if body[:1] in "+-":
|
|
60
|
+
sign = -1.0 if body[0] == "-" else 1.0
|
|
61
|
+
body = body[1:]
|
|
62
|
+
token_re = re.compile(r"([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)?")
|
|
63
|
+
pos = 0
|
|
64
|
+
seconds = 0.0
|
|
65
|
+
matched = False
|
|
66
|
+
for m in token_re.finditer(body):
|
|
67
|
+
if m.start() != pos:
|
|
68
|
+
break
|
|
69
|
+
pos = m.end()
|
|
70
|
+
matched = True
|
|
71
|
+
seconds += float(m.group(1)) * factors[m.group(2) or "s"]
|
|
72
|
+
if not matched or pos != len(body):
|
|
73
|
+
raise ValueError(f"invalid duration: {value}")
|
|
74
|
+
seconds *= sign
|
|
75
|
+
if seconds <= 0:
|
|
76
|
+
raise ValueError("duration must be positive")
|
|
77
|
+
return seconds
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _overlaps_authored_range(ranges, start, end):
|
|
81
|
+
"""Whether [start,end) collides with an already-accepted clip, as AUTHORED.
|
|
82
|
+
|
|
83
|
+
Overlap is judged on the agent's own in/out points, never on the padded ones.
|
|
84
|
+
`clip_padding` deliberately widens every clip by the same amount on both ends, so
|
|
85
|
+
judging padded ranges makes any two back-to-back clips (…, 10) and (10, …) look like
|
|
86
|
+
duplicate footage and hard-fails a perfectly ordinary plan. Padding is an output
|
|
87
|
+
nicety; only what the agent actually asked for defines duplication.
|
|
88
|
+
"""
|
|
89
|
+
return any(start < other_end and end > other_start for other_start, other_end in ranges)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _clip_value(raw, *names):
|
|
93
|
+
for name in names:
|
|
94
|
+
if name in raw:
|
|
95
|
+
return raw[name]
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_clip_plan(path):
|
|
100
|
+
"""Load `clip_plan.json`, accepting either a list or {"clips": [...]} object."""
|
|
101
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _stable_json_dumps(value):
|
|
105
|
+
return json.dumps(
|
|
106
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def value_fingerprint(value):
|
|
111
|
+
"""Return a stable fingerprint for JSON-serializable non-secret values."""
|
|
112
|
+
return hashlib.md5(_stable_json_dumps(value).encode("utf-8")).hexdigest()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cut_plan_fingerprint(validated_plan):
|
|
116
|
+
"""Hash the exact normalized clip plan that determines edited_source.mp4 bytes."""
|
|
117
|
+
if isinstance(validated_plan, dict):
|
|
118
|
+
payload = dict(validated_plan)
|
|
119
|
+
# Provenance for raw-plan freshness is not part of the edited media bytes.
|
|
120
|
+
payload.pop("raw_plan_fingerprint", None)
|
|
121
|
+
# QC is observability derived from the media plan, not an input range decision.
|
|
122
|
+
payload.pop("qc", None)
|
|
123
|
+
else:
|
|
124
|
+
payload = validated_plan
|
|
125
|
+
return value_fingerprint(payload)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_FILE_FINGERPRINT_MEMO = {}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _file_identity(path):
|
|
132
|
+
"""(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
|
|
133
|
+
st = os.stat(os.fspath(path))
|
|
134
|
+
return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def file_fingerprint(path, chunk_size=1024 * 1024):
|
|
138
|
+
"""Return a full-content fingerprint for cache-correct identity checks.
|
|
139
|
+
|
|
140
|
+
The digest covers CONTENT only — never the path or mtime — so a copied video or
|
|
141
|
+
artifact is still recognised as the same asset, while any byte change invalidates
|
|
142
|
+
the cache even if timestamps, size, head, or tail bytes are misleading.
|
|
143
|
+
|
|
144
|
+
Identity metadata is used ONLY to memoize within a single process. One understanding
|
|
145
|
+
run fingerprints the same source video 8-10 times and the whole extracted frame set
|
|
146
|
+
2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
|
|
147
|
+
any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
|
|
148
|
+
re-hashed, so the memo can never serve a stale digest.
|
|
149
|
+
"""
|
|
150
|
+
key = _file_identity(path)
|
|
151
|
+
memoized = _FILE_FINGERPRINT_MEMO.get(key)
|
|
152
|
+
if memoized is not None:
|
|
153
|
+
return memoized
|
|
154
|
+
h = hashlib.sha256()
|
|
155
|
+
with open(os.fspath(path), "rb") as f:
|
|
156
|
+
for chunk in iter(lambda: f.read(chunk_size), b""):
|
|
157
|
+
h.update(chunk)
|
|
158
|
+
digest = h.hexdigest()
|
|
159
|
+
_FILE_FINGERPRINT_MEMO[key] = digest
|
|
160
|
+
return digest
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _edited_source_meta_path(output_path):
|
|
164
|
+
return Path(str(output_path) + ".meta.json")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _load_edited_source_meta(output_path):
|
|
168
|
+
meta_path = _edited_source_meta_path(output_path)
|
|
169
|
+
if not meta_path.exists():
|
|
170
|
+
return None
|
|
171
|
+
try:
|
|
172
|
+
data = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
173
|
+
except (OSError, ValueError):
|
|
174
|
+
return None
|
|
175
|
+
return data if isinstance(data, dict) else None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _source_fingerprints_for_plan(validated_plan, input_video=None):
|
|
179
|
+
"""Fingerprint every media file that can affect edited_source.mp4 bytes."""
|
|
180
|
+
paths = []
|
|
181
|
+
for clip in (
|
|
182
|
+
validated_plan.get("clips", []) if isinstance(validated_plan, dict) else []
|
|
183
|
+
):
|
|
184
|
+
if clip.get("source_path"):
|
|
185
|
+
paths.append(str(clip["source_path"]))
|
|
186
|
+
if not paths and input_video is not None:
|
|
187
|
+
paths.append(str(input_video))
|
|
188
|
+
fingerprints = {}
|
|
189
|
+
for path in sorted(set(paths)):
|
|
190
|
+
fingerprints[str(Path(path))] = file_fingerprint(path)
|
|
191
|
+
return fingerprints
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def edited_source_render_cache_payload():
|
|
195
|
+
"""Render-affecting settings that invalidate edited_source.mp4 cache reuse.
|
|
196
|
+
|
|
197
|
+
Keep this payload limited to inputs/algorithms that can change rendered media bytes.
|
|
198
|
+
Observational QC produced after validation/render is intentionally excluded.
|
|
199
|
+
"""
|
|
200
|
+
return {
|
|
201
|
+
"render_algorithm_version": EDITED_SOURCE_RENDER_ALGORITHM_VERSION,
|
|
202
|
+
"geometry_render_algorithm_version": GEOMETRY_RENDER_ALGORITHM_VERSION,
|
|
203
|
+
"clip_join_audio_fade_ms": round(
|
|
204
|
+
max(0.0, float(CONFIG.get("clip_join_audio_fade_ms", 30.0) or 0.0)), 3
|
|
205
|
+
),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def edited_source_render_fingerprint():
|
|
210
|
+
return value_fingerprint(edited_source_render_cache_payload())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _write_edited_source_meta(output_path, validated_plan, input_video=None):
|
|
214
|
+
meta_path = _edited_source_meta_path(output_path)
|
|
215
|
+
source_fingerprints = _source_fingerprints_for_plan(validated_plan, input_video)
|
|
216
|
+
has_plan_sources = any(
|
|
217
|
+
clip.get("source_path")
|
|
218
|
+
for clip in (
|
|
219
|
+
validated_plan.get("clips", []) if isinstance(validated_plan, dict) else []
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
legacy_source_fp = (
|
|
223
|
+
file_fingerprint(input_video)
|
|
224
|
+
if input_video is not None
|
|
225
|
+
and not has_plan_sources
|
|
226
|
+
and len(source_fingerprints) == 1
|
|
227
|
+
else None
|
|
228
|
+
)
|
|
229
|
+
meta = {
|
|
230
|
+
"schema_version": 2,
|
|
231
|
+
"clip_plan_fingerprint": cut_plan_fingerprint(validated_plan),
|
|
232
|
+
"render_fingerprint": edited_source_render_fingerprint(),
|
|
233
|
+
"render_cache": edited_source_render_cache_payload(),
|
|
234
|
+
"source_fingerprints": source_fingerprints,
|
|
235
|
+
"edited_source_fingerprint": file_fingerprint(output_path),
|
|
236
|
+
"total_duration": validated_plan.get("total_duration"),
|
|
237
|
+
"clip_count": len(validated_plan.get("clips", [])),
|
|
238
|
+
}
|
|
239
|
+
delivery_qc = (validated_plan.get("qc") or {}).get("delivery_qc")
|
|
240
|
+
if delivery_qc:
|
|
241
|
+
meta["delivery_qc"] = delivery_qc
|
|
242
|
+
# Preserve the legacy key for existing single-source callers/tests/metadata readers.
|
|
243
|
+
if legacy_source_fp is not None:
|
|
244
|
+
meta["source_video_fingerprint"] = legacy_source_fp
|
|
245
|
+
meta_path.write_text(
|
|
246
|
+
json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def should_reuse_edited_source(output_path, validated_plan, input_video=None):
|
|
251
|
+
"""Return True only when edited_source.mp4 matches source media and cut params."""
|
|
252
|
+
output_path = Path(output_path)
|
|
253
|
+
if not output_path.exists():
|
|
254
|
+
return False
|
|
255
|
+
meta = _load_edited_source_meta(output_path)
|
|
256
|
+
if not meta or meta.get("clip_plan_fingerprint") != cut_plan_fingerprint(
|
|
257
|
+
validated_plan
|
|
258
|
+
):
|
|
259
|
+
return False
|
|
260
|
+
if meta.get("render_fingerprint") != edited_source_render_fingerprint():
|
|
261
|
+
return False
|
|
262
|
+
expected_sources = _source_fingerprints_for_plan(validated_plan, input_video)
|
|
263
|
+
meta_sources = meta.get("source_fingerprints")
|
|
264
|
+
if meta_sources is None and input_video is not None:
|
|
265
|
+
meta_sources = {str(Path(input_video)): meta.get("source_video_fingerprint")}
|
|
266
|
+
return bool(
|
|
267
|
+
meta_sources == expected_sources
|
|
268
|
+
and meta.get("edited_source_fingerprint") == file_fingerprint(output_path)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _manifest_source_entries(sources_manifest):
|
|
273
|
+
"""Return source rows from common multi-source manifest shapes."""
|
|
274
|
+
if isinstance(sources_manifest, dict):
|
|
275
|
+
if isinstance(sources_manifest.get("sources"), list):
|
|
276
|
+
return sources_manifest["sources"]
|
|
277
|
+
rows = []
|
|
278
|
+
for source_id, value in sources_manifest.items():
|
|
279
|
+
if source_id in {"schema_version", "version"}:
|
|
280
|
+
continue
|
|
281
|
+
if isinstance(value, dict):
|
|
282
|
+
row = dict(value)
|
|
283
|
+
row.setdefault("source_id", source_id)
|
|
284
|
+
rows.append(row)
|
|
285
|
+
if rows:
|
|
286
|
+
return rows
|
|
287
|
+
elif isinstance(sources_manifest, list):
|
|
288
|
+
return sources_manifest
|
|
289
|
+
raise ValueError(
|
|
290
|
+
"sources manifest must be a list, a {sources:[...]} object, or a source_id map"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def normalize_sources_manifest(sources_manifest):
|
|
295
|
+
"""Normalize source manifest rows to {source_id: {source_path, duration}}."""
|
|
296
|
+
sources = {}
|
|
297
|
+
for idx, raw in enumerate(_manifest_source_entries(sources_manifest)):
|
|
298
|
+
if not isinstance(raw, dict):
|
|
299
|
+
raise ValueError(f"source #{idx + 1} must be an object")
|
|
300
|
+
source_id = raw.get("source_id", raw.get("id", raw.get("name")))
|
|
301
|
+
if source_id in (None, ""):
|
|
302
|
+
raise ValueError(f"source #{idx + 1} is missing source_id")
|
|
303
|
+
source_id = str(source_id)
|
|
304
|
+
source_path = raw.get(
|
|
305
|
+
"source_path",
|
|
306
|
+
raw.get("path", raw.get("video_path", raw.get("video", raw.get("file")))),
|
|
307
|
+
)
|
|
308
|
+
if not source_path:
|
|
309
|
+
raise ValueError(f"source {source_id} is missing source_path/path")
|
|
310
|
+
duration = raw.get(
|
|
311
|
+
"duration", raw.get("duration_seconds", raw.get("source_duration"))
|
|
312
|
+
)
|
|
313
|
+
if duration in (None, ""):
|
|
314
|
+
duration = get_video_duration(source_path)
|
|
315
|
+
try:
|
|
316
|
+
duration = max(0.0, float(duration or 0.0))
|
|
317
|
+
except (TypeError, ValueError) as exc:
|
|
318
|
+
raise ValueError(f"source {source_id} has invalid duration") from exc
|
|
319
|
+
sources[source_id] = {
|
|
320
|
+
"source_id": source_id,
|
|
321
|
+
"source_path": str(source_path),
|
|
322
|
+
"duration": duration,
|
|
323
|
+
}
|
|
324
|
+
if raw.get("source_work_dir") not in (None, ""):
|
|
325
|
+
sources[source_id]["source_work_dir"] = str(raw["source_work_dir"])
|
|
326
|
+
if not sources:
|
|
327
|
+
raise ValueError("sources manifest has no sources")
|
|
328
|
+
return sources
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def normalize_multi_source_clip_plan(
|
|
332
|
+
raw_plan,
|
|
333
|
+
sources_manifest,
|
|
334
|
+
target_duration=None,
|
|
335
|
+
clip_padding=0.0,
|
|
336
|
+
min_clip_duration=0.3,
|
|
337
|
+
allow_overlap=False,
|
|
338
|
+
):
|
|
339
|
+
"""Validate a multi-source clip plan and map source_id clips to source paths/durations.
|
|
340
|
+
|
|
341
|
+
Clip order follows the raw plan; overlap validation is isolated per source_id.
|
|
342
|
+
"""
|
|
343
|
+
sources = normalize_sources_manifest(sources_manifest)
|
|
344
|
+
if isinstance(raw_plan, dict):
|
|
345
|
+
raw_clips = raw_plan.get("clips", [])
|
|
346
|
+
plan_target = raw_plan.get("target_duration") or raw_plan.get(
|
|
347
|
+
"target_duration_seconds"
|
|
348
|
+
)
|
|
349
|
+
if target_duration is None and plan_target not in (None, ""):
|
|
350
|
+
target_duration = parse_duration_seconds(plan_target)
|
|
351
|
+
elif isinstance(raw_plan, list):
|
|
352
|
+
raw_clips = raw_plan
|
|
353
|
+
else:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
"clip_plan.json must be a JSON array or an object with a clips array"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if not isinstance(raw_clips, list):
|
|
359
|
+
raise ValueError("clip_plan.json field `clips` must be an array")
|
|
360
|
+
|
|
361
|
+
padding = max(0.0, float(clip_padding or 0.0))
|
|
362
|
+
min_duration = max(0.05, float(min_clip_duration or 0.05))
|
|
363
|
+
clips = []
|
|
364
|
+
source_ranges = {}
|
|
365
|
+
cursor = 0.0
|
|
366
|
+
|
|
367
|
+
for idx, raw in enumerate(raw_clips):
|
|
368
|
+
if not isinstance(raw, dict):
|
|
369
|
+
log(f" 跳过无效 clip #{idx + 1}: not an object")
|
|
370
|
+
continue
|
|
371
|
+
source_id = raw.get("source_id", raw.get("id"))
|
|
372
|
+
if source_id in (None, ""):
|
|
373
|
+
raise ValueError(f"clip #{idx + 1} is missing source_id")
|
|
374
|
+
source_id = str(source_id)
|
|
375
|
+
source = sources.get(source_id)
|
|
376
|
+
if not source:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
f"clip #{idx + 1} references unknown source_id: {source_id}"
|
|
379
|
+
)
|
|
380
|
+
try:
|
|
381
|
+
raw_start = float(_clip_value(raw, "start", "source_start", "in"))
|
|
382
|
+
raw_end = float(_clip_value(raw, "end", "source_end", "out"))
|
|
383
|
+
except (TypeError, ValueError):
|
|
384
|
+
log(f" 跳过无效 clip #{idx + 1}: missing numeric start/end")
|
|
385
|
+
continue
|
|
386
|
+
if raw_end - raw_start < min_duration:
|
|
387
|
+
log(f" 跳过过短 clip #{idx + 1}: {raw_start:.1f}-{raw_end:.1f}s")
|
|
388
|
+
continue
|
|
389
|
+
source_duration = source["duration"]
|
|
390
|
+
start = round(max(0.0, min(raw_start - padding, source_duration)), 3)
|
|
391
|
+
end = round(max(0.0, min(raw_end + padding, source_duration)), 3)
|
|
392
|
+
if end - start < min_duration:
|
|
393
|
+
log(f" 跳过过短 clip #{idx + 1}: {start:.1f}-{end:.1f}s")
|
|
394
|
+
continue
|
|
395
|
+
ranges = source_ranges.setdefault(source_id, [])
|
|
396
|
+
if not allow_overlap and _overlaps_authored_range(ranges, raw_start, raw_end):
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"clip #{idx + 1} overlaps an earlier source range for source_id {source_id}; "
|
|
399
|
+
"split or remove duplicate source footage before mapping narration"
|
|
400
|
+
)
|
|
401
|
+
ranges.append((raw_start, raw_end))
|
|
402
|
+
|
|
403
|
+
duration = round(end - start, 3)
|
|
404
|
+
clip = {
|
|
405
|
+
"clip_id": len(clips),
|
|
406
|
+
"source_id": source_id,
|
|
407
|
+
"source_path": source["source_path"],
|
|
408
|
+
"source_start": start,
|
|
409
|
+
"source_end": end,
|
|
410
|
+
"output_start": round(cursor, 3),
|
|
411
|
+
"output_end": round(cursor + duration, 3),
|
|
412
|
+
"duration": duration,
|
|
413
|
+
"reason": str(raw.get("reason", raw.get("note", ""))).strip(),
|
|
414
|
+
}
|
|
415
|
+
clips.append(clip)
|
|
416
|
+
cursor += duration
|
|
417
|
+
|
|
418
|
+
if not clips:
|
|
419
|
+
raise ValueError("clip_plan.json has no valid clips")
|
|
420
|
+
|
|
421
|
+
total_duration = round(sum(c["duration"] for c in clips), 3)
|
|
422
|
+
plan = {
|
|
423
|
+
"clips": clips,
|
|
424
|
+
"total_duration": total_duration,
|
|
425
|
+
"target_duration": round(float(target_duration), 3)
|
|
426
|
+
if target_duration
|
|
427
|
+
else None,
|
|
428
|
+
"sources": {
|
|
429
|
+
sid: {
|
|
430
|
+
"source_path": s["source_path"],
|
|
431
|
+
"duration": round(s["duration"], 3),
|
|
432
|
+
**(
|
|
433
|
+
{"source_work_dir": s["source_work_dir"]}
|
|
434
|
+
if s.get("source_work_dir")
|
|
435
|
+
else {}
|
|
436
|
+
),
|
|
437
|
+
}
|
|
438
|
+
for sid, s in sources.items()
|
|
439
|
+
},
|
|
440
|
+
"allow_overlap": bool(allow_overlap),
|
|
441
|
+
}
|
|
442
|
+
if target_duration and total_duration > target_duration * 1.15:
|
|
443
|
+
plan["warning"] = (
|
|
444
|
+
f"validated clips total {total_duration:.1f}s exceeds target "
|
|
445
|
+
f"{float(target_duration):.1f}s by more than 15%"
|
|
446
|
+
)
|
|
447
|
+
log(f"警告: {plan['warning']}")
|
|
448
|
+
return plan
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def normalize_clip_plan(
|
|
452
|
+
raw_plan,
|
|
453
|
+
video_duration,
|
|
454
|
+
target_duration=None,
|
|
455
|
+
clip_padding=0.0,
|
|
456
|
+
min_clip_duration=0.3,
|
|
457
|
+
allow_overlap=False,
|
|
458
|
+
):
|
|
459
|
+
"""Validate and enrich an agent-authored clip plan.
|
|
460
|
+
|
|
461
|
+
Returns a dict with validated `clips`, `total_duration`, and target metadata.
|
|
462
|
+
Clip order follows the agent-provided order, so montage ordering is possible.
|
|
463
|
+
"""
|
|
464
|
+
if isinstance(raw_plan, dict):
|
|
465
|
+
raw_clips = raw_plan.get("clips", [])
|
|
466
|
+
plan_target = raw_plan.get("target_duration") or raw_plan.get(
|
|
467
|
+
"target_duration_seconds"
|
|
468
|
+
)
|
|
469
|
+
if target_duration is None and plan_target not in (None, ""):
|
|
470
|
+
target_duration = parse_duration_seconds(plan_target)
|
|
471
|
+
elif isinstance(raw_plan, list):
|
|
472
|
+
raw_clips = raw_plan
|
|
473
|
+
else:
|
|
474
|
+
raise ValueError(
|
|
475
|
+
"clip_plan.json must be a JSON array or an object with a clips array"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if not isinstance(raw_clips, list):
|
|
479
|
+
raise ValueError("clip_plan.json field `clips` must be an array")
|
|
480
|
+
|
|
481
|
+
video_duration = max(0.0, float(video_duration or 0.0))
|
|
482
|
+
padding = max(0.0, float(clip_padding or 0.0))
|
|
483
|
+
min_duration = max(0.05, float(min_clip_duration or 0.05))
|
|
484
|
+
clips = []
|
|
485
|
+
source_ranges = []
|
|
486
|
+
cursor = 0.0
|
|
487
|
+
|
|
488
|
+
for idx, raw in enumerate(raw_clips):
|
|
489
|
+
if not isinstance(raw, dict):
|
|
490
|
+
log(f" 跳过无效 clip #{idx + 1}: not an object")
|
|
491
|
+
continue
|
|
492
|
+
try:
|
|
493
|
+
raw_start = float(_clip_value(raw, "start", "source_start", "in"))
|
|
494
|
+
raw_end = float(_clip_value(raw, "end", "source_end", "out"))
|
|
495
|
+
except (TypeError, ValueError):
|
|
496
|
+
log(f" 跳过无效 clip #{idx + 1}: missing numeric start/end")
|
|
497
|
+
continue
|
|
498
|
+
if raw_end - raw_start < min_duration:
|
|
499
|
+
log(f" 跳过过短 clip #{idx + 1}: {raw_start:.1f}-{raw_end:.1f}s")
|
|
500
|
+
continue
|
|
501
|
+
start = round(max(0.0, min(raw_start - padding, video_duration)), 3)
|
|
502
|
+
end = round(max(0.0, min(raw_end + padding, video_duration)), 3)
|
|
503
|
+
if end - start < min_duration:
|
|
504
|
+
log(f" 跳过过短 clip #{idx + 1}: {start:.1f}-{end:.1f}s")
|
|
505
|
+
continue
|
|
506
|
+
if not allow_overlap and _overlaps_authored_range(source_ranges, raw_start, raw_end):
|
|
507
|
+
raise ValueError(
|
|
508
|
+
f"clip #{idx + 1} overlaps an earlier source range; "
|
|
509
|
+
"split or remove duplicate source footage before mapping narration"
|
|
510
|
+
)
|
|
511
|
+
source_ranges.append((raw_start, raw_end))
|
|
512
|
+
|
|
513
|
+
duration = round(end - start, 3)
|
|
514
|
+
clip = {
|
|
515
|
+
"clip_id": len(clips),
|
|
516
|
+
"source_start": start,
|
|
517
|
+
"source_end": end,
|
|
518
|
+
"output_start": round(cursor, 3),
|
|
519
|
+
"output_end": round(cursor + duration, 3),
|
|
520
|
+
"duration": duration,
|
|
521
|
+
"reason": str(raw.get("reason", raw.get("note", ""))).strip(),
|
|
522
|
+
}
|
|
523
|
+
clips.append(clip)
|
|
524
|
+
cursor += duration
|
|
525
|
+
|
|
526
|
+
if not clips:
|
|
527
|
+
raise ValueError("clip_plan.json has no valid clips")
|
|
528
|
+
|
|
529
|
+
total_duration = round(sum(c["duration"] for c in clips), 3)
|
|
530
|
+
plan = {
|
|
531
|
+
"clips": clips,
|
|
532
|
+
"total_duration": total_duration,
|
|
533
|
+
"target_duration": round(float(target_duration), 3)
|
|
534
|
+
if target_duration
|
|
535
|
+
else None,
|
|
536
|
+
"source_duration": round(video_duration, 3),
|
|
537
|
+
"allow_overlap": bool(allow_overlap),
|
|
538
|
+
}
|
|
539
|
+
if target_duration and total_duration > target_duration * 1.15:
|
|
540
|
+
plan["warning"] = (
|
|
541
|
+
f"validated clips total {total_duration:.1f}s exceeds target "
|
|
542
|
+
f"{float(target_duration):.1f}s by more than 15%"
|
|
543
|
+
)
|
|
544
|
+
log(f"警告: {plan['warning']}")
|
|
545
|
+
return plan
|