@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,795 @@
|
|
|
1
|
+
"""Own recap timeline artifacts, continuation state, and cut QC surfaces."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from recap_runtime import (
|
|
11
|
+
_coerce_videos,
|
|
12
|
+
_entry,
|
|
13
|
+
_load_json,
|
|
14
|
+
_load_run_manifest,
|
|
15
|
+
_multi_run_manifest_payload,
|
|
16
|
+
_run_manifest_payload,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ASSEMBLY_MANIFEST = "assembly_manifest.json"
|
|
20
|
+
|
|
21
|
+
PHASE_LEDGER = "recap_phase.json"
|
|
22
|
+
|
|
23
|
+
CUT_TIMELINE_CRAFT_BULLETS = [
|
|
24
|
+
"- 片段顺序必须服务同一条故事主线,而不是无序高光;可使用 0–1 个 cold open,随后回到 setup → turn → escalation → payoff。",
|
|
25
|
+
"- 每个片段必须对应 `recap_story_plan.json` 中的一个 change-based beat;删除后不损失因果、人物或情绪的片段通常不保留。",
|
|
26
|
+
"- `reason` 统一写成 `beat_id | function | change | POV | preferred moment | 入点 | 出点`,不能只写 hook、重要剧情或事件摘要。",
|
|
27
|
+
"- 优先保留因果、揭示、决定、关系移动、情绪转向与不可替代的表演/反应;跳过片尾、广告、重复静态画面和水印废片段。",
|
|
28
|
+
"- 片段追求最短但完整:建立镜头可以短,关键表演/反应允许多停一点;在完整台词、完整动作或自然声音边界结束,避免原声从半句中切入或切出。",
|
|
29
|
+
"- 新人物/地点/情绪场景先给画面建立空间,再进旁白;不要在原片叠化或闪白中间再切一次。scene score 只用于定位候选接点,最终必须播放接点前后判断。",
|
|
30
|
+
"- 对短时间内密集的 scene 候选先区分来源:原片的无关短镜头整段删除,相关短镜头扩展到完整动作/反应;本次拼接制造的切点优先移动边界、恢复同源连续运动或合并片段,能消除就不保留。",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
MULTI_SOURCE_NARRATION_CRAFT_BULLETS = [
|
|
34
|
+
"- `narration.json` 只使用 edited_source.mp4 的 OUTPUT 时间线,不使用任一原片时间。",
|
|
35
|
+
"- 先为每个 beat 指定 `audio_owner`,再决定是否需要旁白;允许 original_dialogue、action_sound、ambience、music、silence 或 narration 主导。",
|
|
36
|
+
"- 旁白只承担 context、causal_link、foreshadow、interpretation 或 transition;`narration_job=none` 的 beat 不写旁白。",
|
|
37
|
+
"- 7:3 不是配额,只是素材无法给出更好判断时的粗略回退;强对白、动作声、环境或沉默可以完整拥有一个 beat。",
|
|
38
|
+
"- 旁白拥有 beat 时才写成一个连续思路的 BLOCK;句数不是目标,字幕拆 cue 也不能切碎 TTS。不要把每个 beat 都机械变成旁白块或固定原声留白。",
|
|
39
|
+
"- 使用 kept-clip map 与每个 source work_dir 核对人物、事实、ASR 和上下文;跨源转场必须有明确叙事任务。",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
_ALLOWED_VISUAL_OVERLAY_TYPES = {"top_title", "inline_label_or_callout"}
|
|
43
|
+
|
|
44
|
+
_VISUAL_OVERLAYS = "visual_overlays.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _finite_number(value):
|
|
48
|
+
try:
|
|
49
|
+
number = float(value)
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
return None
|
|
52
|
+
return number if math.isfinite(number) else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _iter_narration_segments(narration_data):
|
|
56
|
+
if isinstance(narration_data, list):
|
|
57
|
+
yield from (item for item in narration_data if isinstance(item, dict))
|
|
58
|
+
return
|
|
59
|
+
if not isinstance(narration_data, dict):
|
|
60
|
+
return
|
|
61
|
+
for key in ("segments", "narration", "items"):
|
|
62
|
+
value = narration_data.get(key)
|
|
63
|
+
if isinstance(value, list):
|
|
64
|
+
yield from (item for item in value if isinstance(item, dict))
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _canonical_visual_overlay(overlay, segment=None):
|
|
69
|
+
"""Return the first-release assemble overlay contract, or None.
|
|
70
|
+
|
|
71
|
+
Recap owns the handoff artifact but does not invent richer overlay semantics. Only the
|
|
72
|
+
two overlay types implemented by assemble are allowed through; all unknown platform or
|
|
73
|
+
future types stay out of the canonical first-release file.
|
|
74
|
+
"""
|
|
75
|
+
if not isinstance(overlay, dict):
|
|
76
|
+
return None
|
|
77
|
+
typ = overlay.get("type")
|
|
78
|
+
if typ not in _ALLOWED_VISUAL_OVERLAY_TYPES:
|
|
79
|
+
return None
|
|
80
|
+
text = overlay.get("text")
|
|
81
|
+
if text is None or str(text) == "":
|
|
82
|
+
return None
|
|
83
|
+
seg = segment if isinstance(segment, dict) else {}
|
|
84
|
+
start = _finite_number(overlay.get("start"))
|
|
85
|
+
end = _finite_number(overlay.get("end"))
|
|
86
|
+
if start is None:
|
|
87
|
+
start = _finite_number(seg.get("start"))
|
|
88
|
+
if end is None:
|
|
89
|
+
end = _finite_number(seg.get("end"))
|
|
90
|
+
if start is None or end is None or end <= start:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
item = {"type": typ, "text": str(text), "start": start, "end": end}
|
|
94
|
+
# Preserve renderer-supported placement hints supplied by upstream facts; do not add new
|
|
95
|
+
# semantic overlay kinds or infer platform-specific cards/chapters.
|
|
96
|
+
for key in ("anchor", "x", "y", "max_width", "style"):
|
|
97
|
+
if key in overlay:
|
|
98
|
+
item[key] = overlay[key]
|
|
99
|
+
return item
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_visual_overlays_from_narration(narration_path):
|
|
103
|
+
data = _load_json(narration_path)
|
|
104
|
+
overlays = []
|
|
105
|
+
for segment in _iter_narration_segments(data):
|
|
106
|
+
raw = segment.get("visual_overlays")
|
|
107
|
+
if not isinstance(raw, list):
|
|
108
|
+
continue
|
|
109
|
+
for overlay in raw:
|
|
110
|
+
item = _canonical_visual_overlay(overlay, segment)
|
|
111
|
+
if item is not None:
|
|
112
|
+
overlays.append(item)
|
|
113
|
+
return overlays
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _write_canonical_visual_overlays(work_dir, narration_path):
|
|
117
|
+
"""Write assemble's canonical work_dir/visual_overlays.json recap handoff.
|
|
118
|
+
|
|
119
|
+
Direct assemble still supports user-authored/manual visual_overlays.json files. Once
|
|
120
|
+
recap owns the handoff for a narration, however, the canonical artifact must be a
|
|
121
|
+
deterministic reflection of the current narration so reused work_dirs cannot render
|
|
122
|
+
stale overlays from a previous run. Unsupported/future overlay types are filtered out
|
|
123
|
+
and represented as an explicit empty overlay list.
|
|
124
|
+
"""
|
|
125
|
+
work_dir = Path(work_dir)
|
|
126
|
+
path = work_dir / _VISUAL_OVERLAYS
|
|
127
|
+
overlays = _extract_visual_overlays_from_narration(narration_path)
|
|
128
|
+
payload = {"schema_version": 1, "overlays": overlays}
|
|
129
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
130
|
+
print(f"[video-recap] 🧩 visual overlays: {len(overlays)} → {path}", flush=True)
|
|
131
|
+
return path
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _print_grounding_qc_pointer(work_dir):
|
|
135
|
+
qc_path = Path(work_dir) / "grounding_qc.json"
|
|
136
|
+
if not qc_path.exists():
|
|
137
|
+
return
|
|
138
|
+
data = _load_json(qc_path)
|
|
139
|
+
if isinstance(data, dict):
|
|
140
|
+
verdict = data.get("verdict", "unknown")
|
|
141
|
+
ranges = (data.get("review_coverage") or {}).get("time_ranges") or []
|
|
142
|
+
warnings = data.get("warnings") or []
|
|
143
|
+
suffix = f" · warnings {len(warnings)}" if warnings else ""
|
|
144
|
+
print(
|
|
145
|
+
f"[video-recap] 🧭 Grounding QC: {verdict} · ranges {len(ranges)}{suffix} → {qc_path}",
|
|
146
|
+
flush=True,
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
print(f"[video-recap] 🧭 Grounding QC → {qc_path}", flush=True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _print_narration_review_pointer(work_dir, *, review_ran=True):
|
|
153
|
+
"""Surface the advisory narration review produced by this run, if any.
|
|
154
|
+
|
|
155
|
+
Review is optional/fail-open. Avoid surfacing a stale narration_review.md from an
|
|
156
|
+
older run when review was disabled or failed before producing fresh artifacts.
|
|
157
|
+
"""
|
|
158
|
+
_print_grounding_qc_pointer(work_dir)
|
|
159
|
+
if not review_ran:
|
|
160
|
+
return
|
|
161
|
+
review_md = Path(work_dir) / "narration_review.md"
|
|
162
|
+
if not review_md.exists():
|
|
163
|
+
return
|
|
164
|
+
data = _load_json(Path(work_dir) / "narration_review.json")
|
|
165
|
+
if isinstance(data, dict):
|
|
166
|
+
findings = [f for f in (data.get("findings") or []) if isinstance(f, dict)]
|
|
167
|
+
n_err = sum(1 for f in findings if f.get("severity") == "error")
|
|
168
|
+
tag = str(data.get("verdict") or "见文件")
|
|
169
|
+
print(
|
|
170
|
+
f"[video-recap] 📋 解说评审(建议性,不拦截): {tag} · "
|
|
171
|
+
f"{len(findings)} 条意见(error {n_err})→ {review_md}"
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
print(f"[video-recap] 📋 解说评审意见 → {review_md}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _settings_for_compare(settings):
|
|
178
|
+
"""Settings that, if changed, invalidate reusing an existing work_dir on resume.
|
|
179
|
+
|
|
180
|
+
`consolidate`/`consolidate_asr` are EXCLUDED: they only ADD an optional understanding
|
|
181
|
+
artifact and never re-run Phase A on a Phase-B resume, so a stored manifest carrying the
|
|
182
|
+
old default (or missing the key entirely, pre-dating it) must still resume — otherwise
|
|
183
|
+
flipping `--consolidate`'s default ON would hard-fail every in-flight work_dir.
|
|
184
|
+
"""
|
|
185
|
+
s = dict(settings or {})
|
|
186
|
+
s.pop("consolidate", None)
|
|
187
|
+
s.pop("consolidate_asr", None)
|
|
188
|
+
return s
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _manifest_mismatches(work_dir, video, args):
|
|
192
|
+
expected = _run_manifest_payload(video, args)
|
|
193
|
+
actual = _load_run_manifest(work_dir)
|
|
194
|
+
if not actual:
|
|
195
|
+
return [
|
|
196
|
+
"缺少或无法读取 recap_run_manifest.json;不能证明 work_dir 属于当前视频/参数"
|
|
197
|
+
]
|
|
198
|
+
mismatches = []
|
|
199
|
+
for key in ("source_video", "source_video_fingerprint"):
|
|
200
|
+
if actual.get(key) != expected.get(key):
|
|
201
|
+
mismatches.append(
|
|
202
|
+
f"{key}: expected {expected.get(key)!r}, got {actual.get(key)!r}"
|
|
203
|
+
)
|
|
204
|
+
if _settings_for_compare(actual.get("settings")) != _settings_for_compare(
|
|
205
|
+
expected.get("settings")
|
|
206
|
+
):
|
|
207
|
+
mismatches.append("settings: 当前 CLI/env 参数与 Phase A manifest 不匹配")
|
|
208
|
+
return mismatches
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _multi_manifest_mismatches(work_dir, videos, args, source_records):
|
|
212
|
+
expected = _multi_run_manifest_payload(videos, args, source_records)
|
|
213
|
+
actual = _load_run_manifest(work_dir)
|
|
214
|
+
if not actual:
|
|
215
|
+
return [
|
|
216
|
+
"缺少或无法读取 recap_run_manifest.json;不能证明 work_dir 属于当前多视频/参数"
|
|
217
|
+
]
|
|
218
|
+
mismatches = []
|
|
219
|
+
if actual.get("mode") != "multi_source":
|
|
220
|
+
mismatches.append(f"mode: expected 'multi_source', got {actual.get('mode')!r}")
|
|
221
|
+
expected_sources = [
|
|
222
|
+
{
|
|
223
|
+
"source_id": s.get("source_id"),
|
|
224
|
+
"source_path": s.get("source_path"),
|
|
225
|
+
"source_video_fingerprint": s.get("source_video_fingerprint"),
|
|
226
|
+
}
|
|
227
|
+
for s in expected.get("sources", [])
|
|
228
|
+
]
|
|
229
|
+
actual_sources = [
|
|
230
|
+
{
|
|
231
|
+
"source_id": s.get("source_id"),
|
|
232
|
+
"source_path": s.get("source_path"),
|
|
233
|
+
"source_video_fingerprint": s.get("source_video_fingerprint"),
|
|
234
|
+
}
|
|
235
|
+
for s in actual.get("sources", [])
|
|
236
|
+
if isinstance(s, dict)
|
|
237
|
+
]
|
|
238
|
+
if actual_sources != expected_sources:
|
|
239
|
+
mismatches.append(
|
|
240
|
+
"sources: 当前输入视频列表/顺序/source_id/fingerprint 与 Phase A manifest 不匹配"
|
|
241
|
+
)
|
|
242
|
+
if _settings_for_compare(actual.get("settings")) != _settings_for_compare(
|
|
243
|
+
expected.get("settings")
|
|
244
|
+
):
|
|
245
|
+
mismatches.append("settings: 当前 CLI/env 参数与 Phase A manifest 不匹配")
|
|
246
|
+
return mismatches
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _read_assembly_output(work_dir):
|
|
250
|
+
manifest = _load_json(Path(work_dir) / ASSEMBLY_MANIFEST)
|
|
251
|
+
if isinstance(manifest, dict) and manifest.get("final_output"):
|
|
252
|
+
return Path(manifest["final_output"])
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _file_md5(path):
|
|
257
|
+
path = Path(path)
|
|
258
|
+
return hashlib.md5(path.read_bytes()).hexdigest() if path.exists() else None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _read_phase_ledger(work_dir):
|
|
262
|
+
"""Phase ledger (cut mode): which artifacts exist and the clip_plan/narration they match.
|
|
263
|
+
|
|
264
|
+
Lets resume be driven by recorded phase state rather than bare file existence — the
|
|
265
|
+
prerequisite for the cut-first/narrate-second two-pause flow, and the guard that keeps a
|
|
266
|
+
narration written for one clip_plan from silently driving a different cut into TTS.
|
|
267
|
+
"""
|
|
268
|
+
ledger = _load_json(Path(work_dir) / PHASE_LEDGER)
|
|
269
|
+
return ledger if isinstance(ledger, dict) else None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _write_phase_ledger(work_dir, **fields):
|
|
273
|
+
ledger = _read_phase_ledger(work_dir) or {}
|
|
274
|
+
ledger.update(fields)
|
|
275
|
+
(Path(work_dir) / PHASE_LEDGER).write_text(
|
|
276
|
+
json.dumps(ledger, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
277
|
+
)
|
|
278
|
+
return ledger
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _cut_narration_is_stale(ledger, current_clip_plan_fp):
|
|
282
|
+
"""Two-pass cut: the narration is authored against the rendered cut shown at the A2 pause,
|
|
283
|
+
i.e. against the clip_plan recorded in the ledger. If clip_plan changed since (a re-cut)
|
|
284
|
+
while that narration is still present, it describes the OLD cut — stale."""
|
|
285
|
+
if not ledger:
|
|
286
|
+
return False
|
|
287
|
+
recorded_cp = ledger.get("clip_plan_fingerprint")
|
|
288
|
+
return bool(recorded_cp is not None and recorded_cp != current_clip_plan_fp)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _continuation_command(video, work_dir, args):
|
|
292
|
+
videos = _coerce_videos(video)
|
|
293
|
+
parts = [
|
|
294
|
+
sys.executable,
|
|
295
|
+
str(_entry("video-recap", "recap.py")),
|
|
296
|
+
*[str(v) for v in videos],
|
|
297
|
+
"--work-dir",
|
|
298
|
+
str(work_dir),
|
|
299
|
+
]
|
|
300
|
+
if args.context:
|
|
301
|
+
parts += ["--context", args.context]
|
|
302
|
+
if args.scene_threshold is not None:
|
|
303
|
+
parts += ["--scene-threshold", str(args.scene_threshold)]
|
|
304
|
+
if args.style != "纪录片":
|
|
305
|
+
parts += ["--style", args.style]
|
|
306
|
+
if args.edit_mode != "full":
|
|
307
|
+
parts += ["--edit-mode", args.edit_mode]
|
|
308
|
+
if args.target_duration:
|
|
309
|
+
parts += ["--target-duration", args.target_duration]
|
|
310
|
+
if getattr(args, "allow_duration_drift", False):
|
|
311
|
+
parts.append("--allow-duration-drift")
|
|
312
|
+
if getattr(args, "allow_sparse_cut", False):
|
|
313
|
+
parts.append("--allow-sparse-cut")
|
|
314
|
+
if args.skip_asr:
|
|
315
|
+
parts.append("--skip-asr")
|
|
316
|
+
if args.mimo_video_overview:
|
|
317
|
+
parts.append("--mimo-video-overview")
|
|
318
|
+
mimo_mode = getattr(args, "mimo_qc", "off")
|
|
319
|
+
if mimo_mode != "off":
|
|
320
|
+
parts += ["--mimo-qc", mimo_mode]
|
|
321
|
+
if getattr(args, "mimo_qc_refresh", False):
|
|
322
|
+
parts.append("--mimo-qc-refresh")
|
|
323
|
+
if not args.consolidate: # default is ON now; only the opt-out needs to round-trip
|
|
324
|
+
parts.append("--no-consolidate")
|
|
325
|
+
if args.consolidate_asr:
|
|
326
|
+
parts.append("--consolidate-asr")
|
|
327
|
+
if getattr(args, "mimo_tts_voice", None):
|
|
328
|
+
parts += ["--mimo-tts-voice", args.mimo_tts_voice]
|
|
329
|
+
if getattr(args, "tts_provider", "auto") != "auto":
|
|
330
|
+
parts += ["--tts-provider", args.tts_provider]
|
|
331
|
+
if getattr(args, "voice_ref", None):
|
|
332
|
+
parts += ["--voice-ref", args.voice_ref]
|
|
333
|
+
if getattr(args, "allow_partial_tts", False):
|
|
334
|
+
parts.append("--allow-partial-tts")
|
|
335
|
+
if getattr(args, "burn_subtitles", None) is not None:
|
|
336
|
+
parts.append(
|
|
337
|
+
"--burn-subtitles" if args.burn_subtitles else "--no-burn-subtitles"
|
|
338
|
+
)
|
|
339
|
+
if getattr(args, "subtitle_y_top", None) is not None:
|
|
340
|
+
parts += ["--subtitle-y-top", str(args.subtitle_y_top)]
|
|
341
|
+
if getattr(args, "subtitle_y_bot", None) is not None:
|
|
342
|
+
parts += ["--subtitle-y-bot", str(args.subtitle_y_bot)]
|
|
343
|
+
if getattr(args, "output_dir", None):
|
|
344
|
+
parts += ["--output-dir", args.output_dir]
|
|
345
|
+
if getattr(args, "export_jianying", False):
|
|
346
|
+
parts.append("--export-jianying")
|
|
347
|
+
if getattr(args, "jianying_bundle_media", False):
|
|
348
|
+
parts.append("--jianying-bundle-media")
|
|
349
|
+
if getattr(args, "jianying_no_bundle_media", False):
|
|
350
|
+
parts.append("--jianying-no-bundle-media")
|
|
351
|
+
if getattr(args, "review_narration", None) is not None:
|
|
352
|
+
parts.append(
|
|
353
|
+
"--review-narration" if args.review_narration else "--no-review-narration"
|
|
354
|
+
)
|
|
355
|
+
if getattr(args, "require_narration_review", False):
|
|
356
|
+
parts.append("--require-narration-review")
|
|
357
|
+
if getattr(args, "material_library_dir", None):
|
|
358
|
+
parts += ["--material-library-dir", args.material_library_dir]
|
|
359
|
+
if getattr(args, "use_materials", False):
|
|
360
|
+
parts.append("--use-materials")
|
|
361
|
+
if getattr(args, "save_materials", False):
|
|
362
|
+
parts.append("--save-materials")
|
|
363
|
+
return " ".join(shlex.quote(part) for part in parts)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _source_work_dir(project_work_dir, source_record):
|
|
367
|
+
return Path(project_work_dir) / source_record["source_work_dir"]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _understand_args_for_source(source_record, source_work_dir, args):
|
|
371
|
+
uargs = [
|
|
372
|
+
source_record["source_path"],
|
|
373
|
+
"--work-dir",
|
|
374
|
+
str(source_work_dir),
|
|
375
|
+
"--style",
|
|
376
|
+
args.style,
|
|
377
|
+
]
|
|
378
|
+
if args.context:
|
|
379
|
+
uargs += ["--context", args.context]
|
|
380
|
+
if args.scene_threshold is not None:
|
|
381
|
+
uargs += ["--scene-threshold", str(args.scene_threshold)]
|
|
382
|
+
if args.edit_mode:
|
|
383
|
+
uargs += ["--edit-mode", args.edit_mode]
|
|
384
|
+
if args.target_duration:
|
|
385
|
+
uargs += ["--target-duration", args.target_duration]
|
|
386
|
+
if args.skip_asr:
|
|
387
|
+
uargs.append("--skip-asr")
|
|
388
|
+
if args.mimo_video_overview:
|
|
389
|
+
uargs.append("--mimo-video-overview")
|
|
390
|
+
uargs.append("--consolidate" if args.consolidate else "--no-consolidate")
|
|
391
|
+
if args.consolidate_asr:
|
|
392
|
+
uargs.append("--consolidate-asr")
|
|
393
|
+
return uargs
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _brief_excerpt(path, limit=1200):
|
|
397
|
+
try:
|
|
398
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
399
|
+
except OSError:
|
|
400
|
+
return ""
|
|
401
|
+
if limit <= 0:
|
|
402
|
+
return ""
|
|
403
|
+
if len(text) <= limit:
|
|
404
|
+
return text
|
|
405
|
+
if limit < 8:
|
|
406
|
+
return text[:limit]
|
|
407
|
+
|
|
408
|
+
def section(heading):
|
|
409
|
+
lines = text.splitlines()
|
|
410
|
+
try:
|
|
411
|
+
start = lines.index(heading)
|
|
412
|
+
except ValueError:
|
|
413
|
+
return ""
|
|
414
|
+
end = next(
|
|
415
|
+
(
|
|
416
|
+
index
|
|
417
|
+
for index in range(start + 1, len(lines))
|
|
418
|
+
if lines[index].startswith("## ")
|
|
419
|
+
),
|
|
420
|
+
len(lines),
|
|
421
|
+
)
|
|
422
|
+
return "\n".join(lines[start:end]).strip()
|
|
423
|
+
|
|
424
|
+
def clipped(value, budget):
|
|
425
|
+
if len(value) <= budget:
|
|
426
|
+
return value
|
|
427
|
+
marker = "\n…\n"
|
|
428
|
+
if budget <= len(marker) + 1:
|
|
429
|
+
return value[:budget]
|
|
430
|
+
usable = max(0, budget - len(marker))
|
|
431
|
+
head = max(1, round(usable * 0.7))
|
|
432
|
+
tail = max(0, usable - head)
|
|
433
|
+
suffix = value[-tail:].lstrip() if tail else ""
|
|
434
|
+
return value[:head].rstrip() + marker + suffix
|
|
435
|
+
|
|
436
|
+
# The generic writing contract occupies the front of every generated brief. A raw
|
|
437
|
+
# prefix therefore drops the source facts that multi-source planning actually needs.
|
|
438
|
+
# Give evidence-bearing sections equal space and retain both their opening and tail.
|
|
439
|
+
preferred = [
|
|
440
|
+
section("## Story context (from background_research.json)"),
|
|
441
|
+
section("## Understanding index (from consolidate.py)"),
|
|
442
|
+
section("## ASR writing chunks (semantic windows)"),
|
|
443
|
+
section("## Scene timing guide"),
|
|
444
|
+
]
|
|
445
|
+
preferred = [value for value in preferred if value]
|
|
446
|
+
if preferred:
|
|
447
|
+
separators = 2 * (len(preferred) - 1)
|
|
448
|
+
per_section = max(1, (limit - separators) // len(preferred))
|
|
449
|
+
excerpt = "\n\n".join(clipped(value, per_section) for value in preferred)
|
|
450
|
+
return excerpt[:limit]
|
|
451
|
+
|
|
452
|
+
marker = "\n…\n"
|
|
453
|
+
usable = max(0, limit - len(marker))
|
|
454
|
+
head = max(1, usable // 4)
|
|
455
|
+
tail = max(0, usable - head)
|
|
456
|
+
suffix = text[-tail:].lstrip() if tail else ""
|
|
457
|
+
return text[:head].rstrip() + marker + suffix
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _write_multi_source_clip_brief(work_dir, source_records, args):
|
|
461
|
+
lines = [
|
|
462
|
+
"# Multi-source Clip Plan Brief",
|
|
463
|
+
"",
|
|
464
|
+
"你正在做多视频剪辑复盘。当前 MVP 只支持 `--edit-mode cut`:先做跨素材的故事与视听决定,再写 `clip_plan.json`;下一步会剪出 `edited_source.mp4`,最后按 OUTPUT 时间轴写 `narration.json`。",
|
|
465
|
+
"",
|
|
466
|
+
"## 创作决定",
|
|
467
|
+
"",
|
|
468
|
+
"先判断创作控制模式:CREATE 比较至少两个可行剪辑假设;DIRECTED 落实用户指定结构;REVISION 只改最新反馈点并冻结未点名内容。然后写或更新 `recap_story_plan.json` 与 `visual_audio_board.json`。前者记录观众承诺、POV、戏剧问题、选定主线及 change-based beats;后者记录每拍的具体画面/反应、入点/出点、原声锚点、`audio_owner` 与 `narration_job`。",
|
|
469
|
+
"",
|
|
470
|
+
"多视频不是把每个来源各做一段小总结。每个来源片段都必须服务同一条主线,并用 `source_id` 保留证据归属。这两份计划是 Agent 与建议型评审使用的工作记录,不是 CLI 渲染门禁。",
|
|
471
|
+
"",
|
|
472
|
+
"## 必须写入的格式",
|
|
473
|
+
"",
|
|
474
|
+
"```json",
|
|
475
|
+
'{"target_duration":"10m","clips":[{"source_id":"src_xxx","start":12.0,"end":38.0,"reason":"b01 | hook | knowledge: unknown→threat | POV=主角 | 保留倾听反应 | 入点=问题已问出 | 出点=沉默落地"}]}',
|
|
476
|
+
"```",
|
|
477
|
+
"",
|
|
478
|
+
"- 每个 clip 必须带 `source_id`。",
|
|
479
|
+
"- `start`/`end` 是对应 source 原视频时间(秒)。",
|
|
480
|
+
"- 不同 `source_id` 的相同时间段不算重叠;同一 `source_id` 内不要重复/重叠,除非你明确接受稀疏/重复剪辑风险。",
|
|
481
|
+
'- 素材库是文件系统 JSON/MD/JSONL;需要找历史素材时直接 `grep -R "关键词" <material-library-dir>`。',
|
|
482
|
+
"",
|
|
483
|
+
"## 剪辑规则",
|
|
484
|
+
*CUT_TIMELINE_CRAFT_BULLETS,
|
|
485
|
+
"- 跨来源选择必须服务共享主线;除非本来就是 setup / turn / payoff 的设计,不要让某个来源变成脱节的小复盘。",
|
|
486
|
+
"- 选片前使用下方 source work_dir(`sources/<source_id>`)核对 scenes.json、ASR、索引和逐来源 brief。",
|
|
487
|
+
]
|
|
488
|
+
if args.target_duration:
|
|
489
|
+
lines.append(f"- 目标时长:`{args.target_duration}`。")
|
|
490
|
+
lines += ["", "## Sources", ""]
|
|
491
|
+
for s in source_records:
|
|
492
|
+
swd = _source_work_dir(work_dir, s)
|
|
493
|
+
lines += [
|
|
494
|
+
f"### {s['source_id']} — {s['source_name']}",
|
|
495
|
+
f"- path: `{s['source_path']}`",
|
|
496
|
+
f"- work_dir: `{swd}`",
|
|
497
|
+
f"- fingerprint: `{s['source_video_fingerprint']}`",
|
|
498
|
+
f"- material_id: `{s.get('material_id')}`",
|
|
499
|
+
]
|
|
500
|
+
excerpt = _brief_excerpt(swd / "agent_narration_brief.md")
|
|
501
|
+
if excerpt:
|
|
502
|
+
lines += ["", "#### per-source brief excerpt", "", excerpt, ""]
|
|
503
|
+
(Path(work_dir) / "agent_narration_brief.md").write_text(
|
|
504
|
+
"\n".join(lines).rstrip() + "\n", encoding="utf-8"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
509
|
+
"""Map each source's speech and quiet evidence onto the combined output clock."""
|
|
510
|
+
clips = plan.get("clips", []) if isinstance(plan, dict) else []
|
|
511
|
+
source_by_id = {str(row["source_id"]): row for row in source_records}
|
|
512
|
+
cache = {}
|
|
513
|
+
|
|
514
|
+
def load_source(source_id):
|
|
515
|
+
if source_id in cache:
|
|
516
|
+
return cache[source_id]
|
|
517
|
+
record = source_by_id.get(source_id)
|
|
518
|
+
source_dir = _source_work_dir(work_dir, record) if record else None
|
|
519
|
+
anchors = _load_json(source_dir / "speech_boundary_anchors.json") if source_dir else None
|
|
520
|
+
speech = None
|
|
521
|
+
# Same precedence as audio_mix._handoff_speech_evidence and
|
|
522
|
+
# sentence_boundaries._load_source_speech_spans: the cleaned transcript wins.
|
|
523
|
+
# Reading these in the opposite order made the multi-source output timeline
|
|
524
|
+
# derive its speech spans from a different artifact than every other consumer.
|
|
525
|
+
for name in ("asr_clean.json", "asr_result.json"):
|
|
526
|
+
speech = _load_json(source_dir / name) if source_dir else None
|
|
527
|
+
rows = speech.get("segments", []) if isinstance(speech, dict) else speech
|
|
528
|
+
if isinstance(rows, list) and any(
|
|
529
|
+
isinstance(row, dict) and str(row.get("text") or "").strip()
|
|
530
|
+
for row in rows
|
|
531
|
+
):
|
|
532
|
+
break
|
|
533
|
+
if isinstance(speech, dict):
|
|
534
|
+
speech = speech.get("segments", [])
|
|
535
|
+
quiet = _load_json(source_dir / "silence_periods.json") if source_dir else None
|
|
536
|
+
cache[source_id] = (
|
|
537
|
+
anchors.get("sentence_anchors", []) if isinstance(anchors, dict) else [],
|
|
538
|
+
speech if isinstance(speech, list) else [],
|
|
539
|
+
quiet if isinstance(quiet, list) else [],
|
|
540
|
+
)
|
|
541
|
+
return cache[source_id]
|
|
542
|
+
|
|
543
|
+
mapped_anchors, mapped_speech, mapped_quiet = [], [], []
|
|
544
|
+
for clip in clips:
|
|
545
|
+
source_id = str(clip.get("source_id") or "")
|
|
546
|
+
try:
|
|
547
|
+
source_start = float(clip["source_start"])
|
|
548
|
+
source_end = float(clip["source_end"])
|
|
549
|
+
output_start = float(clip["output_start"])
|
|
550
|
+
except (KeyError, TypeError, ValueError):
|
|
551
|
+
continue
|
|
552
|
+
anchors, speech_rows, quiet_rows = load_source(source_id)
|
|
553
|
+
for anchor in anchors:
|
|
554
|
+
try:
|
|
555
|
+
when = float(anchor.get("time"))
|
|
556
|
+
except (AttributeError, TypeError, ValueError):
|
|
557
|
+
continue
|
|
558
|
+
if source_start - 0.05 <= when <= source_end + 0.05:
|
|
559
|
+
item = dict(anchor)
|
|
560
|
+
try:
|
|
561
|
+
pause = float(item.get("pause_start", when))
|
|
562
|
+
except (TypeError, ValueError):
|
|
563
|
+
pause = when
|
|
564
|
+
if not math.isfinite(pause):
|
|
565
|
+
pause = when
|
|
566
|
+
pause = max(source_start, min(pause, when))
|
|
567
|
+
item.update(
|
|
568
|
+
source_id=source_id,
|
|
569
|
+
source_time=round(when, 3),
|
|
570
|
+
time=round(output_start + when - source_start, 3),
|
|
571
|
+
source_pause_start=round(pause, 3),
|
|
572
|
+
pause_start=round(output_start + pause - source_start, 3),
|
|
573
|
+
)
|
|
574
|
+
mapped_anchors.append(item)
|
|
575
|
+
for rows, destination, require_text in (
|
|
576
|
+
(speech_rows, mapped_speech, True),
|
|
577
|
+
(quiet_rows, mapped_quiet, False),
|
|
578
|
+
):
|
|
579
|
+
for row in rows:
|
|
580
|
+
if not isinstance(row, dict):
|
|
581
|
+
continue
|
|
582
|
+
if require_text and not str(row.get("text") or "").strip():
|
|
583
|
+
continue
|
|
584
|
+
if not require_text and bool(row.get("has_speech", False)):
|
|
585
|
+
continue
|
|
586
|
+
try:
|
|
587
|
+
start = max(source_start, float(row["start"]))
|
|
588
|
+
end = min(source_end, float(row["end"]))
|
|
589
|
+
except (KeyError, TypeError, ValueError):
|
|
590
|
+
continue
|
|
591
|
+
if end <= start:
|
|
592
|
+
continue
|
|
593
|
+
item = dict(row)
|
|
594
|
+
item.update(
|
|
595
|
+
source_id=source_id,
|
|
596
|
+
source_start=round(start, 3),
|
|
597
|
+
source_end=round(end, 3),
|
|
598
|
+
start=round(output_start + start - source_start, 3),
|
|
599
|
+
end=round(output_start + end - source_start, 3),
|
|
600
|
+
)
|
|
601
|
+
destination.append(item)
|
|
602
|
+
|
|
603
|
+
payload = {
|
|
604
|
+
"schema_version": 2,
|
|
605
|
+
"artifact": "speech_boundary_anchors_output.json",
|
|
606
|
+
"timeline": "cut_output",
|
|
607
|
+
"source_artifact": "multi_source_manifest.json",
|
|
608
|
+
"clip_plan_fingerprint": hashlib.md5(
|
|
609
|
+
json.dumps(
|
|
610
|
+
plan, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str
|
|
611
|
+
).encode("utf-8")
|
|
612
|
+
).hexdigest(),
|
|
613
|
+
"sentence_anchors": sorted(mapped_anchors, key=lambda row: float(row["time"])),
|
|
614
|
+
"speech_spans": sorted(mapped_speech, key=lambda row: (row["start"], row["end"])),
|
|
615
|
+
"quiet_windows": sorted(mapped_quiet, key=lambda row: (row["start"], row["end"])),
|
|
616
|
+
}
|
|
617
|
+
Path(work_dir, "speech_boundary_anchors_output.json").write_text(
|
|
618
|
+
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
619
|
+
)
|
|
620
|
+
return payload
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _write_multi_source_output_brief(work_dir, source_records, validated_plan_path):
|
|
624
|
+
plan = _load_json(validated_plan_path)
|
|
625
|
+
clips = plan.get("clips", []) if isinstance(plan, dict) else []
|
|
626
|
+
source_by_id = {s["source_id"]: s for s in source_records}
|
|
627
|
+
speech_evidence = _write_multi_source_output_speech_evidence(
|
|
628
|
+
work_dir, source_records, plan
|
|
629
|
+
)
|
|
630
|
+
lines = [
|
|
631
|
+
"# Multi-source Output Narration Brief",
|
|
632
|
+
"",
|
|
633
|
+
"现在 `edited_source.mp4` 已经由多个源视频剪好。请对剪后成片的 OUTPUT 时间轴写 `narration.json`。",
|
|
634
|
+
"",
|
|
635
|
+
"## 更新创作决定",
|
|
636
|
+
"",
|
|
637
|
+
"先查看 `edited_source.mp4` 与下方 kept-clip map,在 `visual_audio_board.json` 中补齐 OUTPUT 起止时间,并根据实际成片重新确认每拍的 `audio_owner`、原声锚点与 `narration_job`。如剪后顺序改变了主线或情绪路径,同时更新 `recap_story_plan.json`。",
|
|
638
|
+
"",
|
|
639
|
+
"beat 对应关系保留在视听板中;`narration.json` 仍只承载时间、文本与朗读参数,CLI 不声称校验计划映射。",
|
|
640
|
+
"",
|
|
641
|
+
"## narration.json 格式",
|
|
642
|
+
"",
|
|
643
|
+
"```json",
|
|
644
|
+
'[{"start":0.0,"end":4.0,"narration":"解说文本。","pause_after_ms":250,"overlaps_speech":true,"emotion":"平静"}]',
|
|
645
|
+
"```",
|
|
646
|
+
"",
|
|
647
|
+
"注意:`start`/`end` 是剪后成片时间,不是原视频时间。",
|
|
648
|
+
"",
|
|
649
|
+
"## 输出时间线写作规则",
|
|
650
|
+
*MULTI_SOURCE_NARRATION_CRAFT_BULLETS,
|
|
651
|
+
"- 旁白增加上下文、因果、预期、证据支持的解释或跨源过渡,不复述画面像素;先保人物与原声,再润色句子。",
|
|
652
|
+
"",
|
|
653
|
+
"## Kept clips (output → source)",
|
|
654
|
+
]
|
|
655
|
+
for c in clips:
|
|
656
|
+
sid = c.get("source_id")
|
|
657
|
+
src = source_by_id.get(sid, {})
|
|
658
|
+
lines.append(
|
|
659
|
+
f"- output {_fmt_range(c.get('output_start'), c.get('output_end'))} → "
|
|
660
|
+
f"{sid} `{src.get('source_path', c.get('source_path', ''))}` "
|
|
661
|
+
f"source {_fmt_range(c.get('source_start'), c.get('source_end'))} "
|
|
662
|
+
f"{('— ' + str(c.get('reason'))) if c.get('reason') else ''}"
|
|
663
|
+
)
|
|
664
|
+
anchors = speech_evidence["sentence_anchors"]
|
|
665
|
+
if anchors:
|
|
666
|
+
lines += ["", "## 原声句末安全切入点"]
|
|
667
|
+
lines.extend(
|
|
668
|
+
f"- {float(row['time']):.3f}s ({row.get('source_id')})"
|
|
669
|
+
for row in anchors
|
|
670
|
+
if row.get("confidence") in {"high", "medium"}
|
|
671
|
+
)
|
|
672
|
+
lines += ["", "## Source work dirs"]
|
|
673
|
+
for s in source_records:
|
|
674
|
+
lines.append(f"- {s['source_id']}: `{_source_work_dir(work_dir, s)}`")
|
|
675
|
+
(Path(work_dir) / "agent_narration_brief.md").write_text(
|
|
676
|
+
"\n".join(lines).rstrip() + "\n", encoding="utf-8"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _qc_status_is_blocking(value):
|
|
681
|
+
if isinstance(value, str):
|
|
682
|
+
return value.strip().lower() in {
|
|
683
|
+
"blocking",
|
|
684
|
+
"blocked",
|
|
685
|
+
"fail",
|
|
686
|
+
"failed",
|
|
687
|
+
"error",
|
|
688
|
+
}
|
|
689
|
+
return bool(value) if isinstance(value, bool) else False
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _cut_qc_blocking_reasons(qc):
|
|
693
|
+
if not isinstance(qc, dict):
|
|
694
|
+
return []
|
|
695
|
+
reasons = []
|
|
696
|
+
for key in (
|
|
697
|
+
"status",
|
|
698
|
+
"target_duration_status",
|
|
699
|
+
"duration_status",
|
|
700
|
+
"boundary_status",
|
|
701
|
+
):
|
|
702
|
+
if _qc_status_is_blocking(qc.get(key)):
|
|
703
|
+
reasons.append(f"{key}={qc.get(key)}")
|
|
704
|
+
for key in ("blocking", "blocked", "failed", "fail", "errors"):
|
|
705
|
+
value = qc.get(key)
|
|
706
|
+
if value:
|
|
707
|
+
reasons.append(f"{key}={value}")
|
|
708
|
+
checks = qc.get("checks") if isinstance(qc.get("checks"), list) else []
|
|
709
|
+
for item in checks:
|
|
710
|
+
if isinstance(item, dict) and _qc_status_is_blocking(item.get("status")):
|
|
711
|
+
reasons.append(f"{item.get('name', 'check')}={item.get('status')}")
|
|
712
|
+
return reasons
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _cut_qc_summary_lines(qc):
|
|
716
|
+
if not isinstance(qc, dict) or not qc:
|
|
717
|
+
return []
|
|
718
|
+
parts = []
|
|
719
|
+
for key in (
|
|
720
|
+
"target_duration_status",
|
|
721
|
+
"total_duration",
|
|
722
|
+
"clip_count",
|
|
723
|
+
"join_fade_ms",
|
|
724
|
+
):
|
|
725
|
+
if key in qc:
|
|
726
|
+
parts.append(f"{key}={qc.get(key)}")
|
|
727
|
+
geometry = qc.get("output_geometry")
|
|
728
|
+
if isinstance(geometry, dict):
|
|
729
|
+
dims = "x".join(
|
|
730
|
+
str(geometry.get(k))
|
|
731
|
+
for k in ("width", "height")
|
|
732
|
+
if geometry.get(k) is not None
|
|
733
|
+
)
|
|
734
|
+
fps = geometry.get("fps")
|
|
735
|
+
reason = geometry.get("reason") or qc.get("output_geometry_reason")
|
|
736
|
+
fps_suffix = f"@{fps}fps" if fps else ""
|
|
737
|
+
reason_suffix = f" reason={reason}" if reason else ""
|
|
738
|
+
parts.append(f"output_geometry={dims or geometry}{fps_suffix}{reason_suffix}")
|
|
739
|
+
warnings = qc.get("warnings")
|
|
740
|
+
if warnings:
|
|
741
|
+
parts.append(
|
|
742
|
+
f"warnings={len(warnings) if isinstance(warnings, list) else warnings}"
|
|
743
|
+
)
|
|
744
|
+
return ["[video-recap] cut QC: " + "; ".join(parts)] if parts else []
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _surface_cut_qc(work_dir):
|
|
748
|
+
plan = _load_json(Path(work_dir) / "clip_plan_validated.json")
|
|
749
|
+
qc = plan.get("qc") if isinstance(plan, dict) else None
|
|
750
|
+
for line in _cut_qc_summary_lines(qc):
|
|
751
|
+
print(line, flush=True)
|
|
752
|
+
blocking = _cut_qc_blocking_reasons(qc)
|
|
753
|
+
if blocking:
|
|
754
|
+
raise SystemExit("video-cut QC blocking/fail status: " + "; ".join(blocking))
|
|
755
|
+
return qc
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _fmt_range(start, end):
|
|
759
|
+
try:
|
|
760
|
+
return f"{float(start):.3f}-{float(end):.3f}s"
|
|
761
|
+
except (TypeError, ValueError):
|
|
762
|
+
return f"{start}-{end}s"
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _material_library_dir(args):
|
|
766
|
+
return (
|
|
767
|
+
args.material_library_dir
|
|
768
|
+
or os.environ.get("VIDEO_RECAP_MATERIAL_LIBRARY_DIR")
|
|
769
|
+
or None
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _materials_enabled(args):
|
|
774
|
+
return bool(_material_library_dir(args) and args.use_materials)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _save_materials_enabled(args):
|
|
778
|
+
return bool(_material_library_dir(args) and args.save_materials)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
|
|
782
|
+
brief = Path(work_dir) / "agent_narration_brief.md"
|
|
783
|
+
print("=" * 50)
|
|
784
|
+
if brief.exists() and "Research the story FIRST" in brief.read_text(
|
|
785
|
+
encoding="utf-8"
|
|
786
|
+
):
|
|
787
|
+
print(
|
|
788
|
+
"[video-recap] ⚑ 理解素材偏薄:先按 brief 顶部「Research the story FIRST」调研并写 "
|
|
789
|
+
"background_research.json,再写解说,避免看图说话。"
|
|
790
|
+
)
|
|
791
|
+
print(f"[video-recap] ⏸ 阅读 {brief}(按 video-script 规则)后写入 {need_text}")
|
|
792
|
+
if inspect_hint:
|
|
793
|
+
print(f"[video-recap] 先核对状态/时间轴(建议性): {inspect_hint}")
|
|
794
|
+
print(f"[video-recap] 写完后重跑继续: {cont}")
|
|
795
|
+
print("=" * 50)
|