@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,1002 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate the executable cross-document contract of one creator-first episode."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path, PurePosixPath
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from typing import NamedTuple, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MINIMUM_PYTHON = (3, 9)
|
|
16
|
+
if sys.version_info < MINIMUM_PYTHON:
|
|
17
|
+
raise SystemExit(
|
|
18
|
+
"creator_markdown_check.py requires Python {}.{}, running {}.{}".format(
|
|
19
|
+
*MINIMUM_PYTHON, sys.version_info.major, sys.version_info.minor
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
REQUIRED_DOCUMENTS = (
|
|
25
|
+
"剧本.md",
|
|
26
|
+
"视觉设定.md",
|
|
27
|
+
"图片提示词.md",
|
|
28
|
+
"分镜.md",
|
|
29
|
+
"视频提示词.md",
|
|
30
|
+
)
|
|
31
|
+
SECTION_RE = re.compile(r"^## ((?:SHOT|MOTION)-[A-Z0-9-]+)\b", re.MULTILINE)
|
|
32
|
+
IMG_RE = re.compile(r"\b(IMG-[A-Z0-9-]+)《([^》]+)》(控制:([^)]+))")
|
|
33
|
+
# 用途 is optional in the pattern on purpose: a declaration written before this
|
|
34
|
+
# field existed still parses, so the creator gets "REF 缺少用途" instead of the
|
|
35
|
+
# generic syntax error that gives no hint about what to add.
|
|
36
|
+
REF_RE = re.compile(
|
|
37
|
+
r"(REF-[A-Z0-9-]+)(顺序:([1-9]\d*))· "
|
|
38
|
+
r"([^;\n]+?\.(?:png|jpe?g|webp))《([^》\n]+)》"
|
|
39
|
+
r"((?:用途:([^;)\n]+);)?控制:([^;)]+);不得控制:([^)]+))",
|
|
40
|
+
re.IGNORECASE,
|
|
41
|
+
)
|
|
42
|
+
# One picture answers one question. 「同一张图承担两个作用时分开说明」 in
|
|
43
|
+
# references/reference-roles.md is what makes a slot translatable into a
|
|
44
|
+
# provider role, so the vocabulary is closed rather than free prose.
|
|
45
|
+
REF_PURPOSES = (
|
|
46
|
+
"身份",
|
|
47
|
+
"造型状态",
|
|
48
|
+
"地理",
|
|
49
|
+
"构图",
|
|
50
|
+
"尺度",
|
|
51
|
+
"效果",
|
|
52
|
+
"起始帧",
|
|
53
|
+
"结束帧",
|
|
54
|
+
"风格",
|
|
55
|
+
)
|
|
56
|
+
EXPLICIT_TEXT_TO_VIDEO = "无(创作者已明确选择文生视频)"
|
|
57
|
+
PENDING_REFERENCE_SUFFIX_RE = re.compile(r";待补参考图:[^;。\n]+。?$")
|
|
58
|
+
VISUAL_CATEGORIES = ("人物", "造型", "地点", "道具")
|
|
59
|
+
VISUAL_SETTING_LINE_RE = re.compile(
|
|
60
|
+
r"^#{2,4}[ \t ]*(?:" + "|".join(VISUAL_CATEGORIES) + r")[^\n]*$", re.MULTILINE
|
|
61
|
+
)
|
|
62
|
+
VISUAL_SETTING_HEADING_RE = re.compile(
|
|
63
|
+
r"^## (" + "|".join(VISUAL_CATEGORIES) + r") · (.+?)[ \t ]*$", re.MULTILINE
|
|
64
|
+
)
|
|
65
|
+
# Any other `## <word> · <name>` heading, so a 视觉依据 entry that fails to
|
|
66
|
+
# resolve can say "the entry is there, the section word is not one of the four"
|
|
67
|
+
# instead of blaming the storyboard for a `视觉设定.md` problem.
|
|
68
|
+
OTHER_SETTING_HEADING_RE = re.compile(r"^## ([^\n·]+?) · (.+?)[ \t ]*$", re.MULTILINE)
|
|
69
|
+
# 画面代称 is the one field the coverage check depends on, so — like a
|
|
70
|
+
# continuity lock — anything that looks like it has to parse rather than
|
|
71
|
+
# silently drop out.
|
|
72
|
+
# Anchored at the start of the line so a sentence that merely mentions the
|
|
73
|
+
# field is prose, not a malformed declaration.
|
|
74
|
+
SCREEN_NAME_LINE_RE = re.compile(
|
|
75
|
+
r"^[ \t ]*[-*+]?[ \t ]*画面代称[ \t ]*[::][^\n]*$", re.MULTILINE
|
|
76
|
+
)
|
|
77
|
+
SCREEN_NAME_RE = re.compile(
|
|
78
|
+
r"^[ \t ]*[-*+][ \t ]*画面代称[::](.+)$", re.MULTILINE
|
|
79
|
+
)
|
|
80
|
+
VISUAL_BASIS_PREFIX = "《视觉设定.md》·"
|
|
81
|
+
VISUAL_BASIS_ENTRY_RE = re.compile(
|
|
82
|
+
r"(" + "|".join(VISUAL_CATEGORIES) + r")「([^」\n]+)」(控制:([^)\n]+))"
|
|
83
|
+
)
|
|
84
|
+
# A subject a keyframe names but does not show — an owner's abandoned bag, a
|
|
85
|
+
# name on a screen, a homonym of an entry name. SHT-22 excludes these from
|
|
86
|
+
# coverage, so the field needs a way to say so instead of forcing the creator
|
|
87
|
+
# to declare an absent subject as present.
|
|
88
|
+
OFFSCREEN_PREFIX = ";画外:"
|
|
89
|
+
OFFSCREEN_ENTRY_RE = re.compile(
|
|
90
|
+
r"(" + "|".join(VISUAL_CATEGORIES) + r")「([^」\n]+)」"
|
|
91
|
+
)
|
|
92
|
+
# A declared lock must never become a no-op. Anything that *looks* like a lock
|
|
93
|
+
# line -- any list marker, any leading whitespace -- is captured here and then
|
|
94
|
+
# has to parse, so a creator who indents the bullet under 识别锚点 gets an error
|
|
95
|
+
# instead of silent non-enforcement.
|
|
96
|
+
LOCK_LINE_RE = re.compile(r"^[ \t\u3000]*[-*+][ \t\u3000]*连续性锁[::].*$", re.MULTILINE)
|
|
97
|
+
LOCK_RE = re.compile(
|
|
98
|
+
r"^[ \t\u3000]*[-*+][ \t\u3000]*连续性锁:(LOCK-[A-Z0-9-]+)《([^》\n]+)》"
|
|
99
|
+
r"(镜头:([^;)\n]+)"
|
|
100
|
+
r"(?:;图片提示词项:([^;)\n]+))?)"
|
|
101
|
+
r"· 锁面:(.+)$"
|
|
102
|
+
)
|
|
103
|
+
# The surface has to name what is in the picture. A match glued to a negation
|
|
104
|
+
# ("no pale blue sweater") describes what must be absent, so it cannot be the
|
|
105
|
+
# evidence that the fact is present.
|
|
106
|
+
# Chinese runs without spaces, so the CJK markers cannot require a preceding
|
|
107
|
+
# boundary the way the English ones do. A bare 无 is deliberately not a marker:
|
|
108
|
+
# 无袖毛衣 describes the garment rather than excluding it.
|
|
109
|
+
NEGATION_RE = re.compile(
|
|
110
|
+
r"(?:"
|
|
111
|
+
r"(?:^|[\s,;:(\[/—-])(?:no|not|non|never|without|avoid|excludes?|excluding|"
|
|
112
|
+
r"free\s+of|--?no)(?:\s+(?:a|an|the|any|some))?[\s-]*"
|
|
113
|
+
r"|(?:不要|不得|不能|不应|不含|不出现|没有|未|避免|禁止|排除|去掉|移除)"
|
|
114
|
+
r"(?:出现|包含|存在|带|有)?[\s]*"
|
|
115
|
+
r")$",
|
|
116
|
+
re.IGNORECASE,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _sections(document: str, kind: str) -> dict[str, str]:
|
|
121
|
+
matches = [
|
|
122
|
+
match
|
|
123
|
+
for match in SECTION_RE.finditer(document)
|
|
124
|
+
if match.group(1).startswith(f"{kind}-")
|
|
125
|
+
]
|
|
126
|
+
return {
|
|
127
|
+
match.group(1): document[
|
|
128
|
+
match.start() : matches[index + 1].start()
|
|
129
|
+
if index + 1 < len(matches)
|
|
130
|
+
else None
|
|
131
|
+
]
|
|
132
|
+
for index, match in enumerate(matches)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _fields(section: str, *, owner: str, errors: list[str]) -> dict[str, str]:
|
|
137
|
+
pairs = re.findall(r"^- ([^:\n]+):(.+)$", section, re.MULTILINE)
|
|
138
|
+
fields: dict[str, str] = {}
|
|
139
|
+
for key, value in pairs:
|
|
140
|
+
if key in fields:
|
|
141
|
+
errors.append(f"{owner}: 字段重复: {key}")
|
|
142
|
+
fields[key] = value
|
|
143
|
+
return fields
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _plain(value: str) -> str:
|
|
147
|
+
return value.strip().rstrip("。")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _contains_ref_token(value: str) -> bool:
|
|
151
|
+
return "ref-" in value.casefold()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _is_none(value: str) -> bool:
|
|
155
|
+
return not _contains_ref_token(value) and bool(
|
|
156
|
+
re.fullmatch(r"无(?:([^)]+))?", _plain(value))
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _is_explicit_text_to_video(value: str) -> bool:
|
|
161
|
+
return _plain(value) == EXPLICIT_TEXT_TO_VIDEO
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _has_pending_references(value: str) -> bool:
|
|
165
|
+
plain = _plain(value)
|
|
166
|
+
return plain.startswith("无(待补参考图:") or bool(
|
|
167
|
+
PENDING_REFERENCE_SUFFIX_RE.search(value.strip())
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_no_external_reference(value: str) -> bool:
|
|
172
|
+
return not _contains_ref_token(value) and bool(
|
|
173
|
+
re.fullmatch(r"无(?:外部参考)?(?:;[^\n]*)?。?", value.strip())
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _copyable_prompt(
|
|
178
|
+
section: str, heading: str = r"可复制(?:通用)?提示词"
|
|
179
|
+
) -> Optional[str]:
|
|
180
|
+
markers = list(re.finditer(rf"^### {heading}\s*$", section, re.MULTILINE))
|
|
181
|
+
if len(markers) != 1:
|
|
182
|
+
return None
|
|
183
|
+
body = section[markers[0].end() :]
|
|
184
|
+
following = re.search(r"^###\s+|^##\s+", body, re.MULTILINE)
|
|
185
|
+
if following is not None:
|
|
186
|
+
body = body[: following.start()]
|
|
187
|
+
lines = [line for line in body.splitlines() if line.strip()]
|
|
188
|
+
if not lines or any(not line.startswith(">") for line in lines):
|
|
189
|
+
return None
|
|
190
|
+
prompt = "\n".join(line[1:].lstrip() for line in lines).strip()
|
|
191
|
+
return prompt or None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _portable_path(value: str) -> bool:
|
|
195
|
+
if not value or "\\" in value or re.match(r"^[A-Za-z]:", value):
|
|
196
|
+
return False
|
|
197
|
+
parts = value.split("/")
|
|
198
|
+
return not PurePosixPath(value).is_absolute() and not any(
|
|
199
|
+
part in {"", ".", ".."} for part in parts
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _inside(path: Path, root: Path) -> bool:
|
|
204
|
+
resolved = path.resolve()
|
|
205
|
+
return resolved == root or root in resolved.parents
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _references(value: str, owner: str, project_root: Path, errors: list[str]) -> None:
|
|
209
|
+
if _is_none(value):
|
|
210
|
+
return
|
|
211
|
+
reference_value = PENDING_REFERENCE_SUFFIX_RE.sub("", value.strip())
|
|
212
|
+
matches = list(REF_RE.finditer(value))
|
|
213
|
+
cursor = 0
|
|
214
|
+
separators_are_valid = True
|
|
215
|
+
for index, match in enumerate(matches):
|
|
216
|
+
if reference_value[cursor : match.start()] != ("" if index == 0 else ";"):
|
|
217
|
+
separators_are_valid = False
|
|
218
|
+
cursor = match.end()
|
|
219
|
+
trailing = reference_value[cursor:]
|
|
220
|
+
if (
|
|
221
|
+
len(matches) != len(re.findall(r"\bREF-[A-Z0-9-]+\b", value))
|
|
222
|
+
or not matches
|
|
223
|
+
or not separators_are_valid
|
|
224
|
+
or trailing not in {"", "。"}
|
|
225
|
+
):
|
|
226
|
+
# A gap list joined with ; reads as a fourth REF slot and would otherwise
|
|
227
|
+
# be reported as broken REF syntax, whose obvious repair is deleting the
|
|
228
|
+
# gap record -- the silent downgrade this contract exists to prevent.
|
|
229
|
+
if "待补参考图" in value and not _has_pending_references(value):
|
|
230
|
+
errors.append(
|
|
231
|
+
f"{owner}: 待补参考图必须写在最后一个 REF 槽位之后,"
|
|
232
|
+
"缺口之间只用、分隔"
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
errors.append(f"{owner}: 输入参考图必须使用完整 REF 语法")
|
|
236
|
+
return
|
|
237
|
+
refs = [(match.group(1), int(match.group(2)), match.group(3)) for match in matches]
|
|
238
|
+
slots = [item[0] for item in refs]
|
|
239
|
+
orders = [item[1] for item in refs]
|
|
240
|
+
paths = [item[2] for item in refs]
|
|
241
|
+
if len(slots) != len(set(slots)):
|
|
242
|
+
errors.append(f"{owner}: REF 槽位重复")
|
|
243
|
+
if len(orders) != len(set(orders)) or sorted(orders) != list(
|
|
244
|
+
range(1, len(orders) + 1)
|
|
245
|
+
):
|
|
246
|
+
errors.append(f"{owner}: REF 顺序必须唯一且从 1 连续编号")
|
|
247
|
+
purposes = [
|
|
248
|
+
match.group(5).strip() if match.group(5) else "" for match in matches
|
|
249
|
+
]
|
|
250
|
+
path_purposes = list(zip(paths, purposes))
|
|
251
|
+
if len(path_purposes) != len(set(path_purposes)):
|
|
252
|
+
errors.append(f"{owner}: REF 路径与用途完全重复")
|
|
253
|
+
for slot, purpose in zip(slots, purposes):
|
|
254
|
+
if not purpose:
|
|
255
|
+
errors.append(
|
|
256
|
+
f"{owner}: REF 缺少用途: {slot};用途只能是"
|
|
257
|
+
f"{'、'.join(REF_PURPOSES)}其中一个"
|
|
258
|
+
)
|
|
259
|
+
elif purpose not in REF_PURPOSES:
|
|
260
|
+
errors.append(
|
|
261
|
+
f"{owner}: REF 用途不在允许集合内: {slot}({purpose});"
|
|
262
|
+
f"只能是{'、'.join(REF_PURPOSES)}"
|
|
263
|
+
)
|
|
264
|
+
for role in ("起始帧", "结束帧"):
|
|
265
|
+
if purposes.count(role) > 1:
|
|
266
|
+
errors.append(f"{owner}: 同一条目只能有一张{role}参考图")
|
|
267
|
+
if "结束帧" in purposes and "起始帧" not in purposes:
|
|
268
|
+
errors.append(f"{owner}: 绑定结束帧参考图时必须同时绑定起始帧")
|
|
269
|
+
for match, (_, _, raw_path) in zip(matches, refs):
|
|
270
|
+
label, may_control, must_not_control = (
|
|
271
|
+
match.group(4),
|
|
272
|
+
match.group(6),
|
|
273
|
+
match.group(7),
|
|
274
|
+
)
|
|
275
|
+
if not _portable_path(raw_path):
|
|
276
|
+
errors.append(f"{owner}: REF 路径不是安全的项目相对路径: {raw_path}")
|
|
277
|
+
else:
|
|
278
|
+
reference_path = project_root / raw_path
|
|
279
|
+
if not _inside(reference_path, project_root):
|
|
280
|
+
errors.append(f"{owner}: REF 路径越出项目根目录: {raw_path}")
|
|
281
|
+
elif not reference_path.is_file():
|
|
282
|
+
errors.append(f"{owner}: REF 文件不存在: {raw_path}")
|
|
283
|
+
if not re.search(r"[\u4e00-\u9fff]", label):
|
|
284
|
+
errors.append(f"{owner}: REF 缺少中文名称: {match.group(1)}")
|
|
285
|
+
if not may_control.strip() or not must_not_control.strip():
|
|
286
|
+
errors.append(f"{owner}: REF 必须同时声明控制与不得控制: {match.group(1)}")
|
|
287
|
+
allowed = {item.strip() for item in re.split(r"[、,,]", may_control)}
|
|
288
|
+
prohibited = {item.strip() for item in re.split(r"[、,,]", must_not_control)}
|
|
289
|
+
if "" in allowed or "" in prohibited or allowed & prohibited:
|
|
290
|
+
errors.append(f"{owner}: REF 控制与不得控制范围冲突: {match.group(1)}")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _excerpt(value: str, limit: int = 60) -> str:
|
|
294
|
+
"""A short, single-line quote of an offending line for a diagnostic."""
|
|
295
|
+
collapsed = re.sub(r"\s+", " ", value).strip()
|
|
296
|
+
return collapsed if len(collapsed) <= limit else collapsed[: limit - 1] + "…"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _unique(values: "Iterable[str]") -> list[str]:
|
|
300
|
+
seen: set[str] = set()
|
|
301
|
+
result: list[str] = []
|
|
302
|
+
for value in values:
|
|
303
|
+
if value not in seen:
|
|
304
|
+
seen.add(value)
|
|
305
|
+
result.append(value)
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _normalized(value: str) -> str:
|
|
310
|
+
"""Fold case and collapse whitespace so a hard-wrapped prompt still matches.
|
|
311
|
+
|
|
312
|
+
A copyable prompt is one rendered paragraph; the line breaks the repo's
|
|
313
|
+
Markdown style puts in it are not part of the text a creator wrote.
|
|
314
|
+
"""
|
|
315
|
+
return re.sub(r"\s+", " ", value).strip().casefold()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _wordish(character: str) -> bool:
|
|
319
|
+
return bool(character) and character.isascii() and (
|
|
320
|
+
character.isalnum() or character == "-"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _carries_surface(prompt: str, surface: str) -> bool:
|
|
325
|
+
"""Is this lock surface actually asserted by this prompt?
|
|
326
|
+
|
|
327
|
+
Plain containment is not enough. `chipped white enamel mug` must not be
|
|
328
|
+
satisfied by `unchipped ... mug`, and `no pale blue sweater` in a negative
|
|
329
|
+
tail asserts the opposite of the fact the lock exists to hold.
|
|
330
|
+
"""
|
|
331
|
+
haystack = _normalized(prompt)
|
|
332
|
+
needle = _normalized(surface)
|
|
333
|
+
if not needle:
|
|
334
|
+
return False
|
|
335
|
+
start = haystack.find(needle)
|
|
336
|
+
while start != -1:
|
|
337
|
+
end = start + len(needle)
|
|
338
|
+
before = haystack[start - 1] if start else ""
|
|
339
|
+
after = haystack[end] if end < len(haystack) else ""
|
|
340
|
+
# Only ASCII words have boundaries to glue across. Chinese is written
|
|
341
|
+
# without spaces, so treating an adjacent CJK character as "glued" would
|
|
342
|
+
# make a Chinese surface impossible to satisfy.
|
|
343
|
+
glued = (
|
|
344
|
+
(_wordish(needle[:1]) and _wordish(before))
|
|
345
|
+
or (_wordish(needle[-1:]) and _wordish(after))
|
|
346
|
+
)
|
|
347
|
+
if not glued and NEGATION_RE.search(haystack[:start]) is None:
|
|
348
|
+
return True
|
|
349
|
+
start = haystack.find(needle, start + 1)
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class VisualEntry(NamedTuple):
|
|
354
|
+
"""One `视觉设定.md` entry plus every name a prompt body may call it by."""
|
|
355
|
+
|
|
356
|
+
category: str
|
|
357
|
+
name: str
|
|
358
|
+
designators: list[str]
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def key(self) -> tuple[str, str]:
|
|
362
|
+
return (self.category, self.name)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _visual_entries(document: str, errors: list[str]) -> list[VisualEntry]:
|
|
366
|
+
"""Parse the 人物/造型/地点/道具 entries of one `视觉设定.md`.
|
|
367
|
+
|
|
368
|
+
A heading that *looks* like an entry but does not parse would silently drop
|
|
369
|
+
out of the coverage check and then reappear as "视觉依据 names an entry that
|
|
370
|
+
does not exist", so it is rejected here where the cause is visible.
|
|
371
|
+
"""
|
|
372
|
+
for line in VISUAL_SETTING_LINE_RE.findall(document):
|
|
373
|
+
if VISUAL_SETTING_HEADING_RE.match(line) is None:
|
|
374
|
+
errors.append(
|
|
375
|
+
"视觉设定.md: 条目标题必须写成 `## <人物|造型|地点|道具> · <名称>`: "
|
|
376
|
+
+ _excerpt(line)
|
|
377
|
+
)
|
|
378
|
+
for line in SCREEN_NAME_LINE_RE.findall(document):
|
|
379
|
+
if SCREEN_NAME_RE.match(line) is None:
|
|
380
|
+
errors.append(
|
|
381
|
+
"视觉设定.md: 画面代称必须写成 `- 画面代称:<正文里的拼写>`: "
|
|
382
|
+
+ _excerpt(line)
|
|
383
|
+
)
|
|
384
|
+
headings = list(VISUAL_SETTING_HEADING_RE.finditer(document))
|
|
385
|
+
entries: list[VisualEntry] = []
|
|
386
|
+
seen: set[tuple[str, str]] = set()
|
|
387
|
+
for index, heading in enumerate(headings):
|
|
388
|
+
end = (
|
|
389
|
+
headings[index + 1].start()
|
|
390
|
+
if index + 1 < len(headings)
|
|
391
|
+
else len(document)
|
|
392
|
+
)
|
|
393
|
+
category, name = heading.group(1), heading.group(2).strip()
|
|
394
|
+
if not name:
|
|
395
|
+
errors.append(f"视觉设定.md: {category}条目缺少名称")
|
|
396
|
+
continue
|
|
397
|
+
if (category, name) in seen:
|
|
398
|
+
errors.append(f"视觉设定.md: 条目重复: {category}「{name}」")
|
|
399
|
+
continue
|
|
400
|
+
seen.add((category, name))
|
|
401
|
+
declared = SCREEN_NAME_RE.findall(document[heading.end() : end])
|
|
402
|
+
if not declared:
|
|
403
|
+
# No declaration: the entry's own name is the designator, which is
|
|
404
|
+
# what a prompt body written in the project's own language calls it.
|
|
405
|
+
designators = [name]
|
|
406
|
+
elif all(_is_none(_plain(raw)) for raw in declared):
|
|
407
|
+
# `画面代称:无` is the deliberate opt-out for a name too common to
|
|
408
|
+
# match reliably in prose (道具「手机」 against 手机店). It removes the
|
|
409
|
+
# derived name too, otherwise the opt-out would do nothing.
|
|
410
|
+
designators = []
|
|
411
|
+
else:
|
|
412
|
+
designators = []
|
|
413
|
+
for raw in declared:
|
|
414
|
+
value = _plain(raw)
|
|
415
|
+
if _is_none(value):
|
|
416
|
+
continue
|
|
417
|
+
for item in re.split(r"[、,,]", value):
|
|
418
|
+
item = item.strip()
|
|
419
|
+
if item and item not in designators:
|
|
420
|
+
designators.append(item)
|
|
421
|
+
# Only a *declared* designator earns this diagnostic: the creator chose a
|
|
422
|
+
# spelling the matcher cannot honour and deserves to know. A one-character
|
|
423
|
+
# entry name they never declared is simply not name-matched, the same as
|
|
424
|
+
# any entry in a project whose prompt body is written in another language.
|
|
425
|
+
if declared:
|
|
426
|
+
for designator in designators:
|
|
427
|
+
if len(designator.strip()) < 2:
|
|
428
|
+
errors.append(
|
|
429
|
+
f"视觉设定.md: {category}「{name}」的画面代称「{designator}」"
|
|
430
|
+
"过短,无法在正文中可靠识别;请写成至少两个字符,或写「画面代称:无」"
|
|
431
|
+
)
|
|
432
|
+
entries.append(VisualEntry(category, name, designators))
|
|
433
|
+
return entries
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _named_entries(
|
|
437
|
+
prompt: str, entries: list[VisualEntry], *, fold_case: bool = False
|
|
438
|
+
) -> set[tuple[str, str]]:
|
|
439
|
+
"""Which declared entries does this frozen keyframe actually call by name?
|
|
440
|
+
|
|
441
|
+
Longer designators win: a keyframe that shows 「江晨手机」 names the prop, and
|
|
442
|
+
the 人物「江晨」 substring inside it is not a second, unrelated claim.
|
|
443
|
+
"""
|
|
444
|
+
haystack = re.sub(r"\s+", " ", prompt).strip()
|
|
445
|
+
if fold_case:
|
|
446
|
+
haystack = haystack.casefold()
|
|
447
|
+
owners: dict[str, list[VisualEntry]] = {}
|
|
448
|
+
for entry in entries:
|
|
449
|
+
for designator in entry.designators:
|
|
450
|
+
needle = re.sub(r"\s+", " ", designator).strip()
|
|
451
|
+
if fold_case:
|
|
452
|
+
needle = needle.casefold()
|
|
453
|
+
# A one-character designator matches far too much prose to be
|
|
454
|
+
# evidence; _visual_entries already reports it.
|
|
455
|
+
if len(needle) < 2:
|
|
456
|
+
continue
|
|
457
|
+
owners.setdefault(needle, []).append(entry)
|
|
458
|
+
named: set[tuple[str, str]] = set()
|
|
459
|
+
# Spans of every occurrence of a longer designator, whether or not it was
|
|
460
|
+
# credited. 「戒指盒」 occupies its characters even when the sentence is
|
|
461
|
+
# 「没有戒指」, so 道具「戒指」 must not be read out of it.
|
|
462
|
+
taken: list[tuple[int, int]] = []
|
|
463
|
+
for needle in sorted(owners, key=len, reverse=True):
|
|
464
|
+
occurrences: list[tuple[int, int]] = []
|
|
465
|
+
start = haystack.find(needle)
|
|
466
|
+
while start != -1:
|
|
467
|
+
end = start + len(needle)
|
|
468
|
+
occurrences.append((start, end))
|
|
469
|
+
start = haystack.find(needle, start + 1)
|
|
470
|
+
for start, end in occurrences:
|
|
471
|
+
before = haystack[start - 1] if start else ""
|
|
472
|
+
after = haystack[end] if end < len(haystack) else ""
|
|
473
|
+
glued = (
|
|
474
|
+
(_wordish(needle[:1]) and _wordish(before))
|
|
475
|
+
or (_wordish(needle[-1:]) and _wordish(after))
|
|
476
|
+
)
|
|
477
|
+
# Strictly longer, so two entries sharing one designator are both
|
|
478
|
+
# credited instead of the first in document order winning.
|
|
479
|
+
inside_longer = any(
|
|
480
|
+
taken_start <= start
|
|
481
|
+
and end <= taken_end
|
|
482
|
+
and taken_end - taken_start > end - start
|
|
483
|
+
for taken_start, taken_end in taken
|
|
484
|
+
)
|
|
485
|
+
if (
|
|
486
|
+
not glued
|
|
487
|
+
and not inside_longer
|
|
488
|
+
and NEGATION_RE.search(haystack[:start]) is None
|
|
489
|
+
):
|
|
490
|
+
named.update(entry.key for entry in owners[needle])
|
|
491
|
+
taken.extend(occurrences)
|
|
492
|
+
return named
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class VisualBasis(NamedTuple):
|
|
496
|
+
"""One shot's parsed 视觉依据: what it declares, and what it excludes."""
|
|
497
|
+
|
|
498
|
+
parsed: bool
|
|
499
|
+
declared: set[tuple[str, str]]
|
|
500
|
+
offscreen: set[tuple[str, str]]
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def accounted(self) -> set[tuple[str, str]]:
|
|
504
|
+
return self.declared | self.offscreen
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _resolve_entry(
|
|
508
|
+
key: tuple[str, str],
|
|
509
|
+
known: set[tuple[str, str]],
|
|
510
|
+
other_headings: dict[str, str],
|
|
511
|
+
owner: str,
|
|
512
|
+
errors: list[str],
|
|
513
|
+
) -> None:
|
|
514
|
+
if key in known:
|
|
515
|
+
return
|
|
516
|
+
category, name = key
|
|
517
|
+
section = other_headings.get(name)
|
|
518
|
+
if section is not None:
|
|
519
|
+
errors.append(
|
|
520
|
+
f"{owner}: 《视觉设定.md》里有「{name}」,但它的分节词是「{section}」;"
|
|
521
|
+
"条目标题必须写成 `## <人物|造型|地点|道具> · <名称>`"
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
errors.append(
|
|
525
|
+
f"{owner}: 视觉依据指向不存在的《视觉设定.md》条目: {category}「{name}」"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _check_named_coverage(
|
|
530
|
+
prompt: str,
|
|
531
|
+
owner: str,
|
|
532
|
+
where: str,
|
|
533
|
+
basis: "VisualBasis",
|
|
534
|
+
entries: list[VisualEntry],
|
|
535
|
+
errors: list[str],
|
|
536
|
+
) -> None:
|
|
537
|
+
"""Every entry this text calls by name is either in frame or declared 画外."""
|
|
538
|
+
# Matching is case-sensitive so an ordinary English word never impersonates a
|
|
539
|
+
# character called May or Will. A body that writes the name in another case
|
|
540
|
+
# would otherwise fall out of the check silently, so it is reported here.
|
|
541
|
+
named = _named_entries(prompt, entries)
|
|
542
|
+
folded = _named_entries(prompt, entries, fold_case=True)
|
|
543
|
+
for category, name in sorted(folded - named):
|
|
544
|
+
errors.append(
|
|
545
|
+
f"{owner}: {where}里的名字与画面代称大小写不一致: {category}「{name}」;"
|
|
546
|
+
"同一个名字全集只用一个拼写"
|
|
547
|
+
)
|
|
548
|
+
for category, name in sorted(named):
|
|
549
|
+
if (category, name) not in basis.accounted:
|
|
550
|
+
errors.append(
|
|
551
|
+
f"{owner}: {where}写到{category}「{name}」,视觉依据没有覆盖;"
|
|
552
|
+
"本镜确实看不见时在视觉依据末尾加「;画外:"
|
|
553
|
+
f"{category}「{name}」」,正文里这个名字不可靠时在《视觉设定.md》"
|
|
554
|
+
"写「画面代称:无」"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _visual_basis(
|
|
559
|
+
value: str,
|
|
560
|
+
owner: str,
|
|
561
|
+
entries: list[VisualEntry],
|
|
562
|
+
other_headings: dict[str, str],
|
|
563
|
+
errors: list[str],
|
|
564
|
+
) -> VisualBasis:
|
|
565
|
+
"""Parse one shot's 视觉依据 and resolve it against `视觉设定.md`."""
|
|
566
|
+
empty = VisualBasis(True, set(), set())
|
|
567
|
+
if _is_none(value):
|
|
568
|
+
return empty
|
|
569
|
+
plain = _plain(value)
|
|
570
|
+
offscreen_raw = ""
|
|
571
|
+
if OFFSCREEN_PREFIX in plain:
|
|
572
|
+
plain, _, offscreen_raw = plain.partition(OFFSCREEN_PREFIX)
|
|
573
|
+
prefixed = plain.startswith(VISUAL_BASIS_PREFIX)
|
|
574
|
+
body = plain[len(VISUAL_BASIS_PREFIX) :] if prefixed else ""
|
|
575
|
+
matches = list(VISUAL_BASIS_ENTRY_RE.finditer(body)) if prefixed else []
|
|
576
|
+
cursor = 0
|
|
577
|
+
separators_are_valid = True
|
|
578
|
+
for index, match in enumerate(matches):
|
|
579
|
+
# Repeating 《视觉设定.md》· before each entry says the same thing and
|
|
580
|
+
# reads naturally; rejecting it would cost a round trip over nothing.
|
|
581
|
+
separator = body[cursor : match.start()]
|
|
582
|
+
allowed = (
|
|
583
|
+
{""}
|
|
584
|
+
if index == 0
|
|
585
|
+
else {";", ";" + VISUAL_BASIS_PREFIX}
|
|
586
|
+
)
|
|
587
|
+
if separator not in allowed:
|
|
588
|
+
separators_are_valid = False
|
|
589
|
+
cursor = match.end()
|
|
590
|
+
if not prefixed or not matches or not separators_are_valid or body[cursor:]:
|
|
591
|
+
errors.append(
|
|
592
|
+
f"{owner}: 视觉依据必须使用完整语法:"
|
|
593
|
+
"《视觉设定.md》·<人物|造型|地点|道具>「<名称>」(控制:<范围>),多项用;连接"
|
|
594
|
+
)
|
|
595
|
+
# Coverage is not reported on top of a parse failure: every entry would
|
|
596
|
+
# be listed as uncovered and bury the one error that matters.
|
|
597
|
+
return VisualBasis(False, set(), set())
|
|
598
|
+
known = {entry.key for entry in entries}
|
|
599
|
+
declared: set[tuple[str, str]] = set()
|
|
600
|
+
for match in matches:
|
|
601
|
+
key = (match.group(1), match.group(2).strip())
|
|
602
|
+
if key in declared:
|
|
603
|
+
errors.append(f"{owner}: 视觉依据条目重复: {key[0]}「{key[1]}」")
|
|
604
|
+
declared.add(key)
|
|
605
|
+
_resolve_entry(key, known, other_headings, owner, errors)
|
|
606
|
+
if not match.group(3).strip():
|
|
607
|
+
errors.append(f"{owner}: 视觉依据缺少控制范围: {key[0]}「{key[1]}」")
|
|
608
|
+
offscreen: set[tuple[str, str]] = set()
|
|
609
|
+
if offscreen_raw:
|
|
610
|
+
offscreen_raw = offscreen_raw.replace(VISUAL_BASIS_PREFIX, "")
|
|
611
|
+
remainder = OFFSCREEN_ENTRY_RE.sub("", offscreen_raw).strip(";、 ")
|
|
612
|
+
offscreen_matches = list(OFFSCREEN_ENTRY_RE.finditer(offscreen_raw))
|
|
613
|
+
if not offscreen_matches or remainder:
|
|
614
|
+
errors.append(
|
|
615
|
+
f"{owner}: 画外清单必须写成 `;画外:<人物|造型|地点|道具>「<名称>」`,多项用;连接"
|
|
616
|
+
)
|
|
617
|
+
return VisualBasis(False, set(), set())
|
|
618
|
+
for match in offscreen_matches:
|
|
619
|
+
key = (match.group(1), match.group(2).strip())
|
|
620
|
+
if key in declared:
|
|
621
|
+
errors.append(
|
|
622
|
+
f"{owner}: {key[0]}「{key[1]}」同时写进视觉依据和画外清单"
|
|
623
|
+
)
|
|
624
|
+
offscreen.add(key)
|
|
625
|
+
_resolve_entry(key, known, other_headings, owner, errors)
|
|
626
|
+
return VisualBasis(True, declared, offscreen)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class ContinuityLock(NamedTuple):
|
|
630
|
+
"""One declared cross-shot lock: the exact surface and where it applies."""
|
|
631
|
+
|
|
632
|
+
lock_id: str
|
|
633
|
+
surface: str
|
|
634
|
+
shots: list[str]
|
|
635
|
+
images: list[str]
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _continuity_locks(document: str, errors: list[str]) -> list[ContinuityLock]:
|
|
639
|
+
"""Parse the declared continuity locks of one 视觉设定.md."""
|
|
640
|
+
locks: list[ContinuityLock] = []
|
|
641
|
+
seen: set[str] = set()
|
|
642
|
+
for line in LOCK_LINE_RE.findall(document):
|
|
643
|
+
match = LOCK_RE.match(line)
|
|
644
|
+
if match is None:
|
|
645
|
+
errors.append(
|
|
646
|
+
"视觉设定.md: 连续性锁必须使用完整语法: " + _excerpt(line)
|
|
647
|
+
)
|
|
648
|
+
continue
|
|
649
|
+
lock_id, label, scope, image_scope, surface = match.groups()
|
|
650
|
+
if lock_id in seen:
|
|
651
|
+
errors.append(f"{lock_id}: 连续性锁 ID 重复")
|
|
652
|
+
continue
|
|
653
|
+
seen.add(lock_id)
|
|
654
|
+
if not re.search(r"[\u3400-\u9fff]", label):
|
|
655
|
+
errors.append(f"{lock_id}: 连续性锁缺少中文名称")
|
|
656
|
+
surface = _plain(surface)
|
|
657
|
+
if not surface:
|
|
658
|
+
errors.append(f"{lock_id}: 连续性锁缺少锁面")
|
|
659
|
+
continue
|
|
660
|
+
shots = _unique(
|
|
661
|
+
item.strip() for item in re.split(r"[、,,]", _plain(scope)) if item.strip()
|
|
662
|
+
)
|
|
663
|
+
if not shots:
|
|
664
|
+
errors.append(f"{lock_id}: 连续性锁缺少镜头范围")
|
|
665
|
+
continue
|
|
666
|
+
if "全集" in shots and len(shots) != 1:
|
|
667
|
+
errors.append(f"{lock_id}: 连续性锁的镜头范围不能把全集与具体镜头混写")
|
|
668
|
+
continue
|
|
669
|
+
images: list[str] = []
|
|
670
|
+
if image_scope is not None and not _is_none(image_scope):
|
|
671
|
+
images = _unique(
|
|
672
|
+
item.strip()
|
|
673
|
+
for item in re.split(r"[、,,]", _plain(image_scope))
|
|
674
|
+
if item.strip()
|
|
675
|
+
)
|
|
676
|
+
if any(not item.startswith("IMG-") for item in images):
|
|
677
|
+
errors.append(f"{lock_id}: 连续性锁的图片提示词项必须使用 IMG- ID")
|
|
678
|
+
continue
|
|
679
|
+
locks.append(ContinuityLock(lock_id, surface, shots, images))
|
|
680
|
+
return locks
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _prompt_language(project_root: Path) -> str:
|
|
684
|
+
"""The language a copyable prompt body is written in.
|
|
685
|
+
|
|
686
|
+
Mirrors the skills' own routing: the project's declared prompt language,
|
|
687
|
+
falling back to `en` when there is no `short-drama.json` -- which is what
|
|
688
|
+
the storyboard skill tells the keyframe author to assume.
|
|
689
|
+
"""
|
|
690
|
+
configuration = project_root / "short-drama.json"
|
|
691
|
+
if not configuration.is_file():
|
|
692
|
+
return "en"
|
|
693
|
+
try:
|
|
694
|
+
project = json.loads(configuration.read_text(encoding="utf-8"))
|
|
695
|
+
except (OSError, ValueError):
|
|
696
|
+
return "en"
|
|
697
|
+
if not isinstance(project, dict):
|
|
698
|
+
return "en"
|
|
699
|
+
authority = project.get("creator_authority")
|
|
700
|
+
profile = (
|
|
701
|
+
authority.get("production_profile") if isinstance(authority, dict) else None
|
|
702
|
+
)
|
|
703
|
+
if isinstance(profile, dict) and profile.get("status") == "accepted":
|
|
704
|
+
choices = profile.get("choices")
|
|
705
|
+
if isinstance(choices, dict):
|
|
706
|
+
declared = choices.get("video_prompt_language")
|
|
707
|
+
if isinstance(declared, str) and declared:
|
|
708
|
+
return declared
|
|
709
|
+
formats = project.get("format")
|
|
710
|
+
declared = formats.get("prompt_language") if isinstance(formats, dict) else None
|
|
711
|
+
return declared if isinstance(declared, str) and declared else "en"
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _check_language_designators(
|
|
715
|
+
project_root: Path, *, entries: list[VisualEntry], errors: list[str]
|
|
716
|
+
) -> None:
|
|
717
|
+
"""Every character must be nameable in the prompt body's language.
|
|
718
|
+
|
|
719
|
+
Without this, omitting `画面代称` is a silent opt-out of the coverage check
|
|
720
|
+
for exactly the projects that need it -- the keyframe body defaults to `en`
|
|
721
|
+
while `视觉设定.md` is Chinese, which is the shape issue #94 reported.
|
|
722
|
+
|
|
723
|
+
This deliberately does not wait until some shot's 视觉依据 references the
|
|
724
|
+
entry. Gating on that made the two halves of the contract land in different
|
|
725
|
+
rounds: a document missing 视觉依据 got one error, and only after fixing it
|
|
726
|
+
did the missing 画面代称 appear.
|
|
727
|
+
"""
|
|
728
|
+
if _prompt_language(project_root).casefold().startswith("zh"):
|
|
729
|
+
return
|
|
730
|
+
for entry in entries:
|
|
731
|
+
if entry.category != "人物":
|
|
732
|
+
continue
|
|
733
|
+
if entry.designators == [entry.name]:
|
|
734
|
+
errors.append(
|
|
735
|
+
f"视觉设定.md: 人物「{entry.name}」缺少画面代称;"
|
|
736
|
+
"提示词正文不是中文时,写「画面代称:<正文里的拼写>」,"
|
|
737
|
+
"正文从不点名时写「画面代称:无」"
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _check_continuity_locks(
|
|
742
|
+
locks: list[ContinuityLock],
|
|
743
|
+
*,
|
|
744
|
+
shots: dict[str, str],
|
|
745
|
+
motion_by_shot: dict[str, tuple[str, str, Optional[str]]],
|
|
746
|
+
image_prompts: dict[str, Optional[str]],
|
|
747
|
+
errors: list[str],
|
|
748
|
+
) -> None:
|
|
749
|
+
"""Require every declared lock surface to be present where it was scoped."""
|
|
750
|
+
for lock in locks:
|
|
751
|
+
lock_id = lock.lock_id
|
|
752
|
+
surface = lock.surface
|
|
753
|
+
targets = sorted(shots) if lock.shots == ["全集"] else lock.shots
|
|
754
|
+
for shot_id in targets:
|
|
755
|
+
if shot_id not in shots:
|
|
756
|
+
errors.append(f"{lock_id}: 连续性锁指向不存在的镜头: {shot_id}")
|
|
757
|
+
continue
|
|
758
|
+
keyframe = _copyable_prompt(shots[shot_id], heading=r"冻结关键帧提示词")
|
|
759
|
+
if keyframe is None:
|
|
760
|
+
errors.append(f"{lock_id}: {shot_id} 缺少可读的冻结关键帧提示词")
|
|
761
|
+
elif not _carries_surface(keyframe, surface):
|
|
762
|
+
errors.append(f"{lock_id}: {shot_id} 冻结关键帧提示词缺少锁面")
|
|
763
|
+
motion = motion_by_shot.get(shot_id)
|
|
764
|
+
if motion is None:
|
|
765
|
+
continue
|
|
766
|
+
motion_id, _, copyable_prompt = motion
|
|
767
|
+
if copyable_prompt is not None and not _carries_surface(
|
|
768
|
+
copyable_prompt, surface
|
|
769
|
+
):
|
|
770
|
+
errors.append(f"{lock_id}: {motion_id} 可复制提示词缺少锁面")
|
|
771
|
+
for image_id in lock.images:
|
|
772
|
+
if image_id not in image_prompts:
|
|
773
|
+
errors.append(f"{lock_id}: 连续性锁指向不存在的 IMG 条目: {image_id}")
|
|
774
|
+
continue
|
|
775
|
+
image_prompt = image_prompts[image_id]
|
|
776
|
+
if image_prompt is not None and not _carries_surface(image_prompt, surface):
|
|
777
|
+
errors.append(f"{lock_id}: {image_id} 可复制提示词缺少锁面")
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list[str]:
|
|
781
|
+
"""Return all deterministic contract errors for ``episode``."""
|
|
782
|
+
episode = episode.resolve()
|
|
783
|
+
project_root = (project_root or episode.parent.parent).resolve()
|
|
784
|
+
errors: list[str] = []
|
|
785
|
+
missing = [name for name in REQUIRED_DOCUMENTS if not (episode / name).is_file()]
|
|
786
|
+
if missing:
|
|
787
|
+
return [f"缺少创作文档: {', '.join(missing)}"]
|
|
788
|
+
|
|
789
|
+
images = (episode / "图片提示词.md").read_text(encoding="utf-8")
|
|
790
|
+
storyboard = (episode / "分镜.md").read_text(encoding="utf-8")
|
|
791
|
+
video = (episode / "视频提示词.md").read_text(encoding="utf-8")
|
|
792
|
+
visual = (episode / "视觉设定.md").read_text(encoding="utf-8")
|
|
793
|
+
locks = _continuity_locks(visual, errors)
|
|
794
|
+
visual_entries = _visual_entries(visual, errors)
|
|
795
|
+
other_headings = {
|
|
796
|
+
match.group(2).strip(): match.group(1).strip()
|
|
797
|
+
for match in OTHER_SETTING_HEADING_RE.finditer(visual)
|
|
798
|
+
if match.group(1).strip() not in VISUAL_CATEGORIES
|
|
799
|
+
}
|
|
800
|
+
image_pairs = re.findall(r"^## (IMG-[A-Z0-9-]+) · (.+)$", images, re.MULTILINE)
|
|
801
|
+
image_headings = dict(image_pairs)
|
|
802
|
+
all_image_headings = re.findall(r"^## (IMG-[A-Z0-9-]+)\b", images, re.MULTILINE)
|
|
803
|
+
if len(image_pairs) != len(all_image_headings):
|
|
804
|
+
errors.append("图片提示词.md: IMG 标题必须包含中文名称")
|
|
805
|
+
if len(image_pairs) != len(image_headings):
|
|
806
|
+
errors.append("图片提示词.md: IMG 标题 ID 重复")
|
|
807
|
+
for image_id, label in image_pairs:
|
|
808
|
+
if not re.search(r"[\u3400-\u9fff]", label):
|
|
809
|
+
errors.append(f"{image_id}: IMG 标题缺少中文名称")
|
|
810
|
+
image_matches = list(re.finditer(r"^## (IMG-[A-Z0-9-]+)\b", images, re.MULTILINE))
|
|
811
|
+
image_prompts: dict[str, Optional[str]] = {}
|
|
812
|
+
for index, match in enumerate(image_matches):
|
|
813
|
+
body = images[
|
|
814
|
+
match.start() : image_matches[index + 1].start()
|
|
815
|
+
if index + 1 < len(image_matches)
|
|
816
|
+
else None
|
|
817
|
+
]
|
|
818
|
+
reference_value = _fields(body, owner=match.group(1), errors=errors).get(
|
|
819
|
+
"参考", ""
|
|
820
|
+
)
|
|
821
|
+
if not reference_value:
|
|
822
|
+
errors.append(f"{match.group(1)}: 缺少参考字段")
|
|
823
|
+
elif _is_no_external_reference(reference_value):
|
|
824
|
+
pass
|
|
825
|
+
elif "REF-" in reference_value:
|
|
826
|
+
_references(reference_value, match.group(1), project_root, errors)
|
|
827
|
+
else:
|
|
828
|
+
errors.append(
|
|
829
|
+
f"{match.group(1)}: 参考必须声明无外部参考或使用完整 REF 语法"
|
|
830
|
+
)
|
|
831
|
+
image_prompt = _copyable_prompt(body)
|
|
832
|
+
image_prompts[match.group(1)] = image_prompt
|
|
833
|
+
if image_prompt is None:
|
|
834
|
+
errors.append(f"{match.group(1)}: 缺少唯一且非空的可复制提示词")
|
|
835
|
+
|
|
836
|
+
shots = _sections(storyboard, "SHOT")
|
|
837
|
+
motions = _sections(video, "MOTION")
|
|
838
|
+
shot_ids = re.findall(r"^## (SHOT-[A-Z0-9-]+)\b", storyboard, re.MULTILINE)
|
|
839
|
+
motion_ids = re.findall(r"^## (MOTION-[A-Z0-9-]+)\b", video, re.MULTILINE)
|
|
840
|
+
if len(shot_ids) != len(set(shot_ids)):
|
|
841
|
+
errors.append("分镜.md: SHOT 标题 ID 重复")
|
|
842
|
+
if len(motion_ids) != len(set(motion_ids)):
|
|
843
|
+
errors.append("视频提示词.md: MOTION 标题 ID 重复")
|
|
844
|
+
named_shots = re.findall(
|
|
845
|
+
r"^## (SHOT-[A-Z0-9-]+) · ([^\n]+)$", storyboard, re.MULTILINE
|
|
846
|
+
)
|
|
847
|
+
named_motions = re.findall(
|
|
848
|
+
r"^## (MOTION-[A-Z0-9-]+) · ([^\n]+)$", video, re.MULTILINE
|
|
849
|
+
)
|
|
850
|
+
if len(named_shots) != len(shot_ids):
|
|
851
|
+
errors.append("分镜.md: SHOT 标题必须包含中文名称")
|
|
852
|
+
if len(named_motions) != len(motion_ids):
|
|
853
|
+
errors.append("视频提示词.md: MOTION 标题必须包含中文名称")
|
|
854
|
+
for shot_id, label in named_shots:
|
|
855
|
+
if not re.search(r"[\u3400-\u9fff]", label):
|
|
856
|
+
errors.append(f"{shot_id}: SHOT 标题缺少中文名称")
|
|
857
|
+
for motion_id, label in named_motions:
|
|
858
|
+
if not re.search(r"[\u3400-\u9fff]", label):
|
|
859
|
+
errors.append(f"{motion_id}: MOTION 标题缺少中文名称")
|
|
860
|
+
if not shots:
|
|
861
|
+
errors.append("分镜.md: 没有 SHOT 条目")
|
|
862
|
+
if not motions:
|
|
863
|
+
errors.append("视频提示词.md: 没有 MOTION 条目")
|
|
864
|
+
|
|
865
|
+
motion_by_shot: dict[str, tuple[str, str, Optional[str]]] = {}
|
|
866
|
+
for motion_id, body in motions.items():
|
|
867
|
+
fields = _fields(body, owner=motion_id, errors=errors)
|
|
868
|
+
shot_id = _plain(fields.get("分镜", ""))
|
|
869
|
+
copyable_prompt = _copyable_prompt(body)
|
|
870
|
+
if not shot_id:
|
|
871
|
+
errors.append(f"{motion_id}: 缺少分镜字段")
|
|
872
|
+
elif shot_id in motion_by_shot:
|
|
873
|
+
errors.append(f"{motion_id}: 分镜 {shot_id} 被多个 MOTION 引用")
|
|
874
|
+
else:
|
|
875
|
+
motion_by_shot[shot_id] = (motion_id, body, copyable_prompt)
|
|
876
|
+
if motion_id.removeprefix("MOTION-") != shot_id.removeprefix("SHOT-"):
|
|
877
|
+
errors.append(f"{motion_id}: ID 必须与分镜 {shot_id} 一一对应")
|
|
878
|
+
if copyable_prompt is None:
|
|
879
|
+
errors.append(f"{motion_id}: 缺少唯一且非空的可复制提示词")
|
|
880
|
+
|
|
881
|
+
if set(motion_by_shot) != set(shots):
|
|
882
|
+
errors.append("分镜.md/视频提示词.md: SHOT 与 MOTION 未一一对应")
|
|
883
|
+
|
|
884
|
+
for shot_id, shot_body in shots.items():
|
|
885
|
+
fields = _fields(shot_body, owner=shot_id, errors=errors)
|
|
886
|
+
image_value = fields.get("图片提示词项", "")
|
|
887
|
+
if not image_value:
|
|
888
|
+
errors.append(f"{shot_id}: 缺少图片提示词项字段")
|
|
889
|
+
image_refs = IMG_RE.findall(image_value)
|
|
890
|
+
image_remainder = IMG_RE.sub("", image_value).strip(";。 ")
|
|
891
|
+
if not _is_none(image_value) and (
|
|
892
|
+
len(image_refs) != len(re.findall(r"\bIMG-[A-Z0-9-]+\b", image_value))
|
|
893
|
+
or image_remainder
|
|
894
|
+
):
|
|
895
|
+
errors.append(f"{shot_id}: 图片提示词项语法不完整")
|
|
896
|
+
for image_id, label, _ in image_refs:
|
|
897
|
+
if image_id not in image_headings:
|
|
898
|
+
errors.append(f"{shot_id}: IMG 标题不存在: {image_id}")
|
|
899
|
+
elif label != image_headings[image_id]:
|
|
900
|
+
errors.append(f"{shot_id}: IMG 中文名称与标题不一致: {image_id}")
|
|
901
|
+
|
|
902
|
+
shot_input = fields.get("输入参考图", "")
|
|
903
|
+
if not shot_input:
|
|
904
|
+
errors.append(f"{shot_id}: 缺少输入参考图字段")
|
|
905
|
+
_references(shot_input, shot_id, project_root, errors)
|
|
906
|
+
|
|
907
|
+
basis_value = fields.get("视觉依据", "")
|
|
908
|
+
if not basis_value:
|
|
909
|
+
errors.append(f"{shot_id}: 缺少视觉依据字段")
|
|
910
|
+
basis = VisualBasis(False, set(), set())
|
|
911
|
+
else:
|
|
912
|
+
basis = _visual_basis(
|
|
913
|
+
basis_value, shot_id, visual_entries, other_headings, errors
|
|
914
|
+
)
|
|
915
|
+
# The keyframe is the only place the frame's contents exist as text, so a
|
|
916
|
+
# shot without one would make the coverage check below vacuous.
|
|
917
|
+
keyframe = _copyable_prompt(shot_body, heading=r"冻结关键帧提示词")
|
|
918
|
+
if keyframe is None:
|
|
919
|
+
errors.append(f"{shot_id}: 缺少唯一且非空的冻结关键帧提示词")
|
|
920
|
+
elif basis.parsed:
|
|
921
|
+
_check_named_coverage(
|
|
922
|
+
keyframe, shot_id, "冻结关键帧提示词", basis, visual_entries, errors
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
motion = motion_by_shot.get(shot_id)
|
|
926
|
+
if not motion:
|
|
927
|
+
continue
|
|
928
|
+
motion_id, motion_body, copyable_prompt = motion
|
|
929
|
+
motion_fields = _fields(motion_body, owner=motion_id, errors=errors)
|
|
930
|
+
motion_input = motion_fields.get("输入参考图", "")
|
|
931
|
+
_references(motion_input, motion_id, project_root, errors)
|
|
932
|
+
if _plain(motion_input) != _plain(shot_input):
|
|
933
|
+
errors.append(f"{motion_id}: 输入参考图与 {shot_id} 不一致")
|
|
934
|
+
|
|
935
|
+
if _has_pending_references(shot_input):
|
|
936
|
+
errors.append(f"{motion_id}: 仍有待补参考图,不能生成最终视频提示词")
|
|
937
|
+
|
|
938
|
+
has_real_image = not _is_none(shot_input)
|
|
939
|
+
expected_mode = "图生视频" if has_real_image else "文生视频"
|
|
940
|
+
if _plain(motion_fields.get("生成方式", "")) != expected_mode:
|
|
941
|
+
errors.append(f"{motion_id}: 生成方式应为{expected_mode}")
|
|
942
|
+
if not has_real_image:
|
|
943
|
+
if not _is_explicit_text_to_video(shot_input):
|
|
944
|
+
errors.append(
|
|
945
|
+
f"{motion_id}: 无真实输入参考图时不能静默降级为文生视频;"
|
|
946
|
+
"请先绑定已有图片、列出待补图片,或记录创作者已明确选择文生视频"
|
|
947
|
+
)
|
|
948
|
+
anchor = _plain(motion_fields.get("静态视觉锚点", ""))
|
|
949
|
+
if not anchor or anchor == "无":
|
|
950
|
+
errors.append(f"{motion_id}: 文生视频缺少静态视觉锚点")
|
|
951
|
+
if (
|
|
952
|
+
copyable_prompt is not None
|
|
953
|
+
and anchor
|
|
954
|
+
and anchor != "无"
|
|
955
|
+
and anchor not in copyable_prompt
|
|
956
|
+
):
|
|
957
|
+
errors.append(f"{motion_id}: 可复制提示词没有包含静态视觉锚点")
|
|
958
|
+
# In text-to-video the anchor carries the appearance the keyframe
|
|
959
|
+
# would otherwise have carried, so it is the same claim about the
|
|
960
|
+
# same frame and answers to the same visual basis. The rest of the
|
|
961
|
+
# motion body is not checked: it may legitimately name an offscreen
|
|
962
|
+
# speaker, which SHT-22 excludes.
|
|
963
|
+
if anchor and anchor != "无" and basis.parsed:
|
|
964
|
+
_check_named_coverage(
|
|
965
|
+
anchor, motion_id, "静态视觉锚点", basis, visual_entries, errors
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
_check_language_designators(
|
|
969
|
+
project_root, entries=visual_entries, errors=errors
|
|
970
|
+
)
|
|
971
|
+
_check_continuity_locks(
|
|
972
|
+
locks,
|
|
973
|
+
shots=shots,
|
|
974
|
+
motion_by_shot=motion_by_shot,
|
|
975
|
+
image_prompts=image_prompts,
|
|
976
|
+
errors=errors,
|
|
977
|
+
)
|
|
978
|
+
return errors
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def main() -> int:
|
|
982
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
983
|
+
parser.add_argument("episode", type=Path, help="包含五份 Markdown 的剧集目录")
|
|
984
|
+
parser.add_argument("--project-root", type=Path, help="用于解析 REF 项目相对路径")
|
|
985
|
+
args = parser.parse_args()
|
|
986
|
+
# 诊断与剧集路径都是中文。stdout 重定向时 Windows 用 ANSI 代码页,默认的
|
|
987
|
+
# strict 处理器会在打印这一步抛错;stderr 早就是 backslashreplace,这里让
|
|
988
|
+
# stdout 用同一个处理器:能编码就照常显示中文,不能编码才退成转义。
|
|
989
|
+
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
|
990
|
+
if reconfigure is not None:
|
|
991
|
+
reconfigure(errors="backslashreplace")
|
|
992
|
+
errors = validate_episode(args.episode, args.project_root)
|
|
993
|
+
if errors:
|
|
994
|
+
for error in errors:
|
|
995
|
+
print(f"ERROR: {error}")
|
|
996
|
+
return 1
|
|
997
|
+
print(f"OK: {args.episode}")
|
|
998
|
+
return 0
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
if __name__ == "__main__":
|
|
1002
|
+
raise SystemExit(main())
|