@oh-story/dsh 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (479) hide show
  1. package/README.md +56 -12
  2. package/lib/client.js +2707 -268
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +136 -101
  5. package/lib/drama/skills/short-drama/SKILL.md +61 -1
  6. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
  8. package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
  9. package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
  10. package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
  11. package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
  12. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  13. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  14. package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
  15. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  16. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
  17. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
  18. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  19. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
  21. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
  22. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
  23. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
  24. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
  25. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  26. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  27. package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
  28. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  29. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  30. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  31. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  32. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  33. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  34. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
  35. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  36. package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
  37. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  38. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
  39. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  40. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
  41. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
  42. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
  43. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  44. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
  45. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  46. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  47. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
  50. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
  51. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  53. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
  54. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  55. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  56. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  57. package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
  58. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  59. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  60. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  61. package/lib/index.js +865 -33
  62. package/lib/index.js.map +4 -4
  63. package/lib/novel-to-game/LICENSE +21 -0
  64. package/lib/novel-to-game/README.md +180 -0
  65. package/lib/novel-to-game/README_ZH.md +178 -0
  66. package/lib/novel-to-game/VERSION +1 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  91. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  92. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  93. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  94. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  95. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  96. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  97. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  98. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  99. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  100. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  101. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  102. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  103. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  104. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  128. package/lib/novel-to-game/manifest.json +508 -0
  129. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  130. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  131. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  132. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  133. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  134. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  135. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  136. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  137. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  138. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  139. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  140. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  141. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  142. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  143. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  144. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  145. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  146. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  147. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  148. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  149. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  150. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  151. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  152. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  153. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  154. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  155. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  156. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  157. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  158. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  159. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  160. package/lib/oh-story/manifest.json +415 -305
  161. package/lib/oh-story/roles/consistency-checker.md +1 -1
  162. package/lib/oh-story/roles/narrative-writer.md +32 -33
  163. package/lib/oh-story/roles/story-architect.md +16 -26
  164. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  165. package/lib/oh-story/skills/story/SKILL.md +16 -9
  166. package/lib/oh-story/skills/story/VERSION +1 -1
  167. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  168. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  169. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  170. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  171. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  172. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  173. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  174. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  175. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  176. package/lib/oh-story/skills/story-import/SKILL.md +19 -10
  177. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  180. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  181. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  182. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  183. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  184. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  185. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  186. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  187. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  188. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  189. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  190. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  191. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  192. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  193. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  194. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  195. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  196. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  197. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  198. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
  199. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
  200. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  201. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  202. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  203. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  204. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  205. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  206. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
  207. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  208. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
  209. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  210. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  211. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  212. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  213. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  214. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  215. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  216. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  217. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  218. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  219. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  220. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  221. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  222. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  223. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  224. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  225. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  226. package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
  227. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  228. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  229. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  230. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  236. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  237. package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
  238. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  239. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  243. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  244. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  245. package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
  246. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  247. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  248. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  249. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  250. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  251. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  252. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  253. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  254. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  255. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  257. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  258. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  259. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  260. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  261. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  262. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  263. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  264. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  265. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  266. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  267. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  268. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  270. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  271. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  272. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  273. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  274. 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
  275. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  276. 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
  277. 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
  278. 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
  279. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  280. 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
  281. 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
  282. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  283. 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
  284. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  285. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  286. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  287. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  288. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  289. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  290. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  291. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  292. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  293. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  294. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  295. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  296. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  297. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  298. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  299. package/lib/types/client/drama-production-view.d.ts +34 -0
  300. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  301. package/lib/types/client/drama-production.d.ts +92 -0
  302. package/lib/types/client/drama-production.d.ts.map +1 -0
  303. package/lib/types/client/file-activity.d.ts +4 -2
  304. package/lib/types/client/file-activity.d.ts.map +1 -1
  305. package/lib/types/client/index.d.ts +1 -1
  306. package/lib/types/client/index.d.ts.map +1 -1
  307. package/lib/types/client/production-intents.d.ts +9 -0
  308. package/lib/types/client/production-intents.d.ts.map +1 -0
  309. package/lib/types/client/production-prompts.d.ts +9 -0
  310. package/lib/types/client/production-prompts.d.ts.map +1 -0
  311. package/lib/types/client/production-runtime.d.ts +58 -0
  312. package/lib/types/client/production-runtime.d.ts.map +1 -0
  313. package/lib/types/client/video-studio.d.ts +41 -0
  314. package/lib/types/client/video-studio.d.ts.map +1 -0
  315. package/lib/types/client/workbench-ui.d.ts +5 -0
  316. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  317. package/lib/types/game-verification.d.ts +16 -0
  318. package/lib/types/game-verification.d.ts.map +1 -0
  319. package/lib/types/index.d.ts +3 -1
  320. package/lib/types/index.d.ts.map +1 -1
  321. package/lib/types/production-intent.d.ts +21 -0
  322. package/lib/types/production-intent.d.ts.map +1 -0
  323. package/lib/types/production-tool.d.ts +5 -0
  324. package/lib/types/production-tool.d.ts.map +1 -0
  325. package/lib/types/role-tool.d.ts.map +1 -1
  326. package/lib/types/skill-provider.d.ts +7 -0
  327. package/lib/types/skill-provider.d.ts.map +1 -1
  328. package/lib/types/video-project.d.ts +42 -0
  329. package/lib/types/video-project.d.ts.map +1 -0
  330. package/lib/types/workspace-request-trust.d.ts +6 -0
  331. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  332. package/lib/types/workspace-route.d.ts +20 -0
  333. package/lib/types/workspace-route.d.ts.map +1 -1
  334. package/lib/video-recap/CHANGELOG.md +282 -0
  335. package/lib/video-recap/LICENSE +21 -0
  336. package/lib/video-recap/README.en.md +257 -0
  337. package/lib/video-recap/README.md +255 -0
  338. package/lib/video-recap/manifest.json +684 -0
  339. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  340. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  341. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  342. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  343. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  344. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  345. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  346. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  347. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  348. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  349. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  350. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  351. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  352. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  353. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  354. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  355. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  356. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  357. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  358. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  359. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  360. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  361. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  362. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  363. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  364. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  365. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  366. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  367. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  368. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  369. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  370. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  371. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  372. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  373. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  374. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  375. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  376. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  377. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  378. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  379. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  380. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  381. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  382. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  383. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  384. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  385. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  386. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  387. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  388. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  389. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  390. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  391. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  392. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  393. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  394. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  395. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  396. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  397. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  398. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  399. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  400. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  401. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  402. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  403. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  404. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  405. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  406. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  407. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  408. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  409. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  410. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  411. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  412. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  413. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  414. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  415. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  416. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  417. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  418. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  419. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  420. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  421. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  422. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  423. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  424. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  425. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  426. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  427. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  428. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  429. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  430. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  431. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  432. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  433. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  434. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  435. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  436. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  437. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  438. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  439. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  440. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  441. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  442. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  443. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  444. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  445. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  446. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  447. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  448. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  449. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  450. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  451. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  452. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  453. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  454. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  455. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  456. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  457. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  458. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  459. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  460. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  461. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  462. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  463. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  464. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  465. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  466. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  467. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  468. package/package.json +55 -40
  469. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  470. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  471. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  472. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  473. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  474. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  475. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  476. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  477. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  478. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  479. /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())