@lemoncat7/dsh-partner 2.4.2 → 2.7.0

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 (671) hide show
  1. package/README.md +8 -0
  2. package/docs/THIRD_PARTY_LICENSES.md +13 -0
  3. package/docs/attachment-storage.md +18 -0
  4. package/docs/director-skills.md +24 -0
  5. package/docs/long-task-continuation.md +51 -0
  6. package/docs/releases/2.5.0.md +13 -0
  7. package/docs/releases/2.6.0.md +11 -0
  8. package/docs/releases/2.7.0.md +21 -0
  9. package/lib/agent-runtime.d.ts +9 -1
  10. package/lib/agent-runtime.d.ts.map +1 -1
  11. package/lib/agent-runtime.js +117 -17
  12. package/lib/agent-runtime.js.map +1 -1
  13. package/lib/api/features/attachments-api.d.ts.map +1 -1
  14. package/lib/api/features/attachments-api.js +31 -1
  15. package/lib/api/features/attachments-api.js.map +1 -1
  16. package/lib/api.d.ts +1 -1
  17. package/lib/api.d.ts.map +1 -1
  18. package/lib/api.js +1 -1
  19. package/lib/api.js.map +1 -1
  20. package/lib/attachments/grouped-storage.d.ts +5 -0
  21. package/lib/attachments/grouped-storage.d.ts.map +1 -0
  22. package/lib/attachments/grouped-storage.js +47 -0
  23. package/lib/attachments/grouped-storage.js.map +1 -0
  24. package/lib/attachments/service.d.ts +17 -0
  25. package/lib/attachments/service.d.ts.map +1 -1
  26. package/lib/attachments/service.js +100 -8
  27. package/lib/attachments/service.js.map +1 -1
  28. package/lib/attachments/shared-storage.d.ts +7 -0
  29. package/lib/attachments/shared-storage.d.ts.map +1 -0
  30. package/lib/attachments/shared-storage.js +44 -0
  31. package/lib/attachments/shared-storage.js.map +1 -0
  32. package/lib/attachments/storage-admin.d.ts +6 -0
  33. package/lib/attachments/storage-admin.d.ts.map +1 -0
  34. package/lib/attachments/storage-admin.js +43 -0
  35. package/lib/attachments/storage-admin.js.map +1 -0
  36. package/lib/attachments/storage-types.d.ts +33 -0
  37. package/lib/attachments/storage-types.d.ts.map +1 -0
  38. package/lib/attachments/storage-types.js +2 -0
  39. package/lib/attachments/storage-types.js.map +1 -0
  40. package/lib/attachments/tool.d.ts.map +1 -1
  41. package/lib/attachments/tool.js +16 -5
  42. package/lib/attachments/tool.js.map +1 -1
  43. package/lib/channels/delivery-policy.d.ts.map +1 -1
  44. package/lib/channels/delivery-policy.js +7 -4
  45. package/lib/channels/delivery-policy.js.map +1 -1
  46. package/lib/channels/direct/message-format.js +418 -281
  47. package/lib/channels/direct/message-format.js.map +4 -4
  48. package/lib/channels/manager.d.ts +7 -3
  49. package/lib/channels/manager.d.ts.map +1 -1
  50. package/lib/channels/manager.js +73 -38
  51. package/lib/channels/manager.js.map +1 -1
  52. package/lib/channels/weixin/api.d.ts.map +1 -1
  53. package/lib/channels/weixin/api.js +7 -2
  54. package/lib/channels/weixin/api.js.map +1 -1
  55. package/lib/channels/weixin/errors.d.ts +14 -0
  56. package/lib/channels/weixin/errors.d.ts.map +1 -0
  57. package/lib/channels/weixin/errors.js +29 -0
  58. package/lib/channels/weixin/errors.js.map +1 -0
  59. package/lib/channels/weixin/poll.d.ts +14 -0
  60. package/lib/channels/weixin/poll.d.ts.map +1 -0
  61. package/lib/channels/weixin/poll.js +50 -0
  62. package/lib/channels/weixin/poll.js.map +1 -0
  63. package/lib/client-api.d.ts +1 -23
  64. package/lib/client-api.d.ts.map +1 -1
  65. package/lib/client-api.js.map +1 -1
  66. package/lib/client-controller.d.ts.map +1 -1
  67. package/lib/client-controller.js +3 -3
  68. package/lib/client-controller.js.map +1 -1
  69. package/lib/client.d.ts.map +1 -1
  70. package/lib/client.js +1078 -679
  71. package/lib/client.js.map +4 -4
  72. package/lib/collaboration/composition.d.ts.map +1 -1
  73. package/lib/collaboration/composition.js +32 -8
  74. package/lib/collaboration/composition.js.map +1 -1
  75. package/lib/collaboration/domain.d.ts +8 -0
  76. package/lib/collaboration/domain.d.ts.map +1 -1
  77. package/lib/collaboration/domain.js.map +1 -1
  78. package/lib/collaboration/service.d.ts.map +1 -1
  79. package/lib/collaboration/service.js +38 -5
  80. package/lib/collaboration/service.js.map +1 -1
  81. package/lib/concern-notification.d.ts.map +1 -1
  82. package/lib/concern-notification.js +2 -1
  83. package/lib/concern-notification.js.map +1 -1
  84. package/lib/concern-tool.js +1 -1
  85. package/lib/concern-tool.js.map +1 -1
  86. package/lib/execution/reply-progress.d.ts.map +1 -1
  87. package/lib/execution/reply-progress.js +4 -2
  88. package/lib/execution/reply-progress.js.map +1 -1
  89. package/lib/execution/service.d.ts +1 -1
  90. package/lib/execution/service.d.ts.map +1 -1
  91. package/lib/execution/service.js +4 -7
  92. package/lib/execution/service.js.map +1 -1
  93. package/lib/index.d.ts.map +1 -1
  94. package/lib/index.js +7 -1
  95. package/lib/index.js.map +1 -1
  96. package/lib/memory-reflection.js +2 -2
  97. package/lib/memory-reflection.js.map +1 -1
  98. package/lib/message-source.d.ts +25 -0
  99. package/lib/message-source.d.ts.map +1 -0
  100. package/lib/message-source.js +8 -0
  101. package/lib/message-source.js.map +1 -0
  102. package/lib/observation-loop.js +4 -4
  103. package/lib/observation-loop.js.map +1 -1
  104. package/lib/pendant/reader.js +1 -1
  105. package/lib/pendant/reader.js.map +1 -1
  106. package/lib/pendant/settings-panel.js +1 -1
  107. package/lib/pendant/settings-panel.js.map +1 -1
  108. package/lib/persona/service.js +1 -1
  109. package/lib/persona/service.js.map +1 -1
  110. package/lib/requirements/attachments.d.ts +7 -0
  111. package/lib/requirements/attachments.d.ts.map +1 -0
  112. package/lib/requirements/attachments.js +26 -0
  113. package/lib/requirements/attachments.js.map +1 -0
  114. package/lib/requirements/domain.d.ts +1 -1
  115. package/lib/requirements/domain.d.ts.map +1 -1
  116. package/lib/requirements/service.d.ts.map +1 -1
  117. package/lib/requirements/service.js +4 -1
  118. package/lib/requirements/service.js.map +1 -1
  119. package/lib/scheduler/channel-origin.d.ts +7 -0
  120. package/lib/scheduler/channel-origin.d.ts.map +1 -0
  121. package/lib/scheduler/channel-origin.js +27 -0
  122. package/lib/scheduler/channel-origin.js.map +1 -0
  123. package/lib/scheduler/continuations.d.ts +29 -0
  124. package/lib/scheduler/continuations.d.ts.map +1 -0
  125. package/lib/scheduler/continuations.js +286 -0
  126. package/lib/scheduler/continuations.js.map +1 -0
  127. package/lib/scheduler/domain.d.ts +40 -0
  128. package/lib/scheduler/domain.d.ts.map +1 -1
  129. package/lib/scheduler/final-reply.d.ts +13 -0
  130. package/lib/scheduler/final-reply.d.ts.map +1 -0
  131. package/lib/scheduler/final-reply.js +21 -0
  132. package/lib/scheduler/final-reply.js.map +1 -0
  133. package/lib/scheduler/service.d.ts +2 -0
  134. package/lib/scheduler/service.d.ts.map +1 -1
  135. package/lib/scheduler/service.js +30 -1
  136. package/lib/scheduler/service.js.map +1 -1
  137. package/lib/scheduler/wakeup-context.d.ts +7 -0
  138. package/lib/scheduler/wakeup-context.d.ts.map +1 -0
  139. package/lib/scheduler/wakeup-context.js +14 -0
  140. package/lib/scheduler/wakeup-context.js.map +1 -0
  141. package/lib/scheduler/wakeup.d.ts +11 -0
  142. package/lib/scheduler/wakeup.d.ts.map +1 -0
  143. package/lib/scheduler/wakeup.js +52 -0
  144. package/lib/scheduler/wakeup.js.map +1 -0
  145. package/lib/skills/builtin-resources.d.ts +6 -0
  146. package/lib/skills/builtin-resources.d.ts.map +1 -0
  147. package/lib/skills/builtin-resources.js +47 -0
  148. package/lib/skills/builtin-resources.js.map +1 -0
  149. package/lib/skills/builtin.d.ts +2 -5
  150. package/lib/skills/builtin.d.ts.map +1 -1
  151. package/lib/skills/builtin.js +11 -0
  152. package/lib/skills/builtin.js.map +1 -1
  153. package/lib/skills/director-builtins.d.ts +8 -0
  154. package/lib/skills/director-builtins.d.ts.map +1 -0
  155. package/lib/skills/director-builtins.js +65 -0
  156. package/lib/skills/director-builtins.js.map +1 -0
  157. package/lib/skills/drama-builtin.d.ts +3 -0
  158. package/lib/skills/drama-builtin.d.ts.map +1 -0
  159. package/lib/skills/drama-builtin.js +49 -0
  160. package/lib/skills/drama-builtin.js.map +1 -0
  161. package/lib/skills/long-task-continuation.d.ts +4 -0
  162. package/lib/skills/long-task-continuation.d.ts.map +1 -0
  163. package/lib/skills/long-task-continuation.js +43 -0
  164. package/lib/skills/long-task-continuation.js.map +1 -0
  165. package/lib/skills/service.d.ts.map +1 -1
  166. package/lib/skills/service.js +8 -3
  167. package/lib/skills/service.js.map +1 -1
  168. package/lib/skills/viora-canvas-builtin.d.ts +3 -0
  169. package/lib/skills/viora-canvas-builtin.d.ts.map +1 -0
  170. package/lib/skills/viora-canvas-builtin.js +19 -0
  171. package/lib/skills/viora-canvas-builtin.js.map +1 -0
  172. package/lib/storage/migrate-layout.d.ts.map +1 -1
  173. package/lib/storage/migrate-layout.js +13 -4
  174. package/lib/storage/migrate-layout.js.map +1 -1
  175. package/lib/store.d.ts.map +1 -1
  176. package/lib/store.js +2 -0
  177. package/lib/store.js.map +1 -1
  178. package/lib/tasks/context.js +1 -0
  179. package/lib/tasks/context.js.map +1 -1
  180. package/lib/tasks/continuation-yield.d.ts +4 -0
  181. package/lib/tasks/continuation-yield.d.ts.map +1 -0
  182. package/lib/tasks/continuation-yield.js +27 -0
  183. package/lib/tasks/continuation-yield.js.map +1 -0
  184. package/lib/tasks/continuation.d.ts +13 -0
  185. package/lib/tasks/continuation.d.ts.map +1 -0
  186. package/lib/tasks/continuation.js +106 -0
  187. package/lib/tasks/continuation.js.map +1 -0
  188. package/lib/tasks/domain.d.ts +2 -0
  189. package/lib/tasks/domain.d.ts.map +1 -1
  190. package/lib/tasks/scheduling.d.ts.map +1 -1
  191. package/lib/tasks/scheduling.js +7 -0
  192. package/lib/tasks/scheduling.js.map +1 -1
  193. package/lib/tasks/service.d.ts +2 -1
  194. package/lib/tasks/service.d.ts.map +1 -1
  195. package/lib/tasks/service.js +19 -1
  196. package/lib/tasks/service.js.map +1 -1
  197. package/lib/ui/attachment-storage.d.ts +2 -0
  198. package/lib/ui/attachment-storage.d.ts.map +1 -0
  199. package/lib/ui/attachment-storage.js +59 -0
  200. package/lib/ui/attachment-storage.js.map +1 -0
  201. package/lib/ui/capability-editor.js +1 -1
  202. package/lib/ui/capability-editor.js.map +1 -1
  203. package/lib/ui/companion-access-panel.js +1 -1
  204. package/lib/ui/companion-access-panel.js.map +1 -1
  205. package/lib/ui/companion-create.js +1 -1
  206. package/lib/ui/companion-create.js.map +1 -1
  207. package/lib/ui/companion-picker.js +1 -1
  208. package/lib/ui/companion-picker.js.map +1 -1
  209. package/lib/ui/identity-editor.js +1 -1
  210. package/lib/ui/identity-editor.js.map +1 -1
  211. package/lib/ui/mcp-capabilities.js +1 -1
  212. package/lib/ui/mcp-capabilities.js.map +1 -1
  213. package/lib/ui/mcp-panel.js +1 -1
  214. package/lib/ui/mcp-panel.js.map +1 -1
  215. package/lib/ui/memory/concern-board.js +2 -2
  216. package/lib/ui/memory/concern-board.js.map +1 -1
  217. package/lib/ui/memory/concern-deletion.d.ts +1 -1
  218. package/lib/ui/memory/concern-deletion.d.ts.map +1 -1
  219. package/lib/ui/memory/concern-deletion.js +9 -4
  220. package/lib/ui/memory/concern-deletion.js.map +1 -1
  221. package/lib/ui/memory/concern-panel.js +1 -1
  222. package/lib/ui/memory/concern-panel.js.map +1 -1
  223. package/lib/ui/memory/memory-library.js +1 -1
  224. package/lib/ui/memory/memory-library.js.map +1 -1
  225. package/lib/ui/memory/memory-panel.js +1 -1
  226. package/lib/ui/memory/memory-panel.js.map +1 -1
  227. package/lib/ui/partner-components.js +1 -1
  228. package/lib/ui/partner-components.js.map +1 -1
  229. package/lib/ui/requirement-board.js +1 -1
  230. package/lib/ui/requirement-board.js.map +1 -1
  231. package/lib/ui/requirement-dialog.js +1 -1
  232. package/lib/ui/requirement-dialog.js.map +1 -1
  233. package/lib/ui/requirement-tasks-panel.js +1 -1
  234. package/lib/ui/requirement-tasks-panel.js.map +1 -1
  235. package/lib/ui/schedule-panel.d.ts.map +1 -1
  236. package/lib/ui/schedule-panel.js +20 -4
  237. package/lib/ui/schedule-panel.js.map +1 -1
  238. package/lib/ui/skills-panel.js +1 -1
  239. package/lib/ui/skills-panel.js.map +1 -1
  240. package/lib/ui/storage-settings.d.ts.map +1 -1
  241. package/lib/ui/storage-settings.js +2 -1
  242. package/lib/ui/storage-settings.js.map +1 -1
  243. package/lib/ui/task-board-stage.js +1 -1
  244. package/lib/ui/task-board-stage.js.map +1 -1
  245. package/lib/ui/workspace-components.js +1 -1
  246. package/lib/ui/workspace-components.js.map +1 -1
  247. package/lib/ui/workspace-navigation.d.ts +16 -16
  248. package/lib/ui/workspace-navigation.js +1 -1
  249. package/lib/ui/workspace-navigation.js.map +1 -1
  250. package/package.json +55 -41
  251. package/resources/skills/cinematic-director/manifest.json +205 -0
  252. package/resources/skills/cinematic-director/upstream/CHANGELOG.md +213 -0
  253. package/resources/skills/cinematic-director/upstream/CONTRIBUTING.md +316 -0
  254. package/resources/skills/cinematic-director/upstream/LICENSE +21 -0
  255. package/resources/skills/cinematic-director/upstream/README.md +507 -0
  256. package/resources/skills/cinematic-director/upstream/SKILL.md +420 -0
  257. package/resources/skills/cinematic-director/upstream/assets/beat-sheet-template.md +97 -0
  258. package/resources/skills/cinematic-director/upstream/assets/director-book-template.md +254 -0
  259. package/resources/skills/cinematic-director/upstream/assets/edit-timeline-template.md +147 -0
  260. package/resources/skills/cinematic-director/upstream/assets/keyframe-prompt-template.md +334 -0
  261. package/resources/skills/cinematic-director/upstream/assets/qc-checklist.md +153 -0
  262. package/resources/skills/cinematic-director/upstream/assets/shot-plan-template.md +95 -0
  263. package/resources/skills/cinematic-director/upstream/assets/sound-plan-template.md +181 -0
  264. package/resources/skills/cinematic-director/upstream/assets/video-prompt-template.md +305 -0
  265. package/resources/skills/cinematic-director/upstream/evals/run.md +69 -0
  266. package/resources/skills/cinematic-director/upstream/references/ai-video-tool-adapters.md +527 -0
  267. package/resources/skills/cinematic-director/upstream/references/blocking-and-staging.md +424 -0
  268. package/resources/skills/cinematic-director/upstream/references/cinematic-language.md +595 -0
  269. package/resources/skills/cinematic-director/upstream/references/continuity-bible.md +410 -0
  270. package/resources/skills/cinematic-director/upstream/references/director_styles/01_spielberg.md +192 -0
  271. package/resources/skills/cinematic-director/upstream/references/director_styles/02_hitchcock.md +193 -0
  272. package/resources/skills/cinematic-director/upstream/references/director_styles/03_kubrick.md +196 -0
  273. package/resources/skills/cinematic-director/upstream/references/director_styles/04_kurosawa.md +196 -0
  274. package/resources/skills/cinematic-director/upstream/references/director_styles/05_scorsese.md +195 -0
  275. package/resources/skills/cinematic-director/upstream/references/director_styles/06_fellini.md +197 -0
  276. package/resources/skills/cinematic-director/upstream/references/director_styles/07_bergman.md +197 -0
  277. package/resources/skills/cinematic-director/upstream/references/director_styles/08_tarkovsky.md +184 -0
  278. package/resources/skills/cinematic-director/upstream/references/director_styles/09_wong_kar_wai.md +189 -0
  279. package/resources/skills/cinematic-director/upstream/references/director_styles/10_nolan.md +185 -0
  280. package/resources/skills/cinematic-director/upstream/references/director_styles/11_villeneuve.md +188 -0
  281. package/resources/skills/cinematic-director/upstream/references/director_styles/12_fincher.md +185 -0
  282. package/resources/skills/cinematic-director/upstream/references/director_styles/13_refn.md +186 -0
  283. package/resources/skills/cinematic-director/upstream/references/director_styles/14_bi_gan.md +188 -0
  284. package/resources/skills/cinematic-director/upstream/references/director_styles/15_zhang_yimou.md +192 -0
  285. package/resources/skills/cinematic-director/upstream/references/director_styles/16_hou_hsiao_hsien.md +192 -0
  286. package/resources/skills/cinematic-director/upstream/references/director_styles/17_park_chan_wook.md +192 -0
  287. package/resources/skills/cinematic-director/upstream/references/director_styles/18_malick.md +191 -0
  288. package/resources/skills/cinematic-director/upstream/references/director_styles/19_michael_mann.md +192 -0
  289. package/resources/skills/cinematic-director/upstream/references/director_styles/20_coen_brothers.md +194 -0
  290. package/resources/skills/cinematic-director/upstream/references/director_styles/README.md +89 -0
  291. package/resources/skills/cinematic-director/upstream/references/director_styles/example_comparisons.md +140 -0
  292. package/resources/skills/cinematic-director/upstream/references/editing-and-assembly.md +263 -0
  293. package/resources/skills/cinematic-director/upstream/references/failure-modes.md +594 -0
  294. package/resources/skills/cinematic-director/upstream/references/genre-playbooks.md +407 -0
  295. package/resources/skills/cinematic-director/upstream/references/image-model-adapters.md +290 -0
  296. package/resources/skills/cinematic-director/upstream/references/lighting-and-color.md +349 -0
  297. package/resources/skills/cinematic-director/upstream/references/product-and-macro.md +207 -0
  298. package/resources/skills/cinematic-director/upstream/references/production-workflow.md +238 -0
  299. package/resources/skills/cinematic-director/upstream/references/prompt-lexicon.md +602 -0
  300. package/resources/skills/cinematic-director/upstream/references/sound-and-dialogue.md +364 -0
  301. package/resources/skills/director-skills/manifest.json +541 -0
  302. package/resources/skills/director-skills/upstream/CHANGELOG.md +32 -0
  303. package/resources/skills/director-skills/upstream/LICENSE +21 -0
  304. package/resources/skills/director-skills/upstream/MANIFEST.md +366 -0
  305. package/resources/skills/director-skills/upstream/README.md +251 -0
  306. package/resources/skills/director-skills/upstream/RESEARCH_PROVENANCE.md +37 -0
  307. package/resources/skills/director-skills/upstream/SKILL.md +114 -0
  308. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/SKILL.md +81 -0
  309. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/checklists/continuity_checklist.md +8 -0
  310. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/examples/character_continuity_update.md +9 -0
  311. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/examples/location_continuity_update.md +9 -0
  312. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/continuity_system.md +21 -0
  313. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/golden_image_workflow.md +29 -0
  314. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/identity_anchors.md +14 -0
  315. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/visual_bible_system.md +15 -0
  316. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/character_profile.md +15 -0
  317. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/continuity_checklist.md +11 -0
  318. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/costume_profile.md +12 -0
  319. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/location_profile.md +14 -0
  320. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/prop_profile.md +13 -0
  321. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/visual_style_bible.md +15 -0
  322. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/tests/continuity_tests.md +6 -0
  323. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/SKILL.md +80 -0
  324. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/checklists/image_prompt_quality_checklist.md +10 -0
  325. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/examples/shot_list_to_image_prompts.md +4 -0
  326. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/image_prompt_components.md +22 -0
  327. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/lighting_and_photorealism.md +19 -0
  328. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/negative_prompt_patterns.md +18 -0
  329. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/prompt_anatomy.md +18 -0
  330. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/character_reference_sheet_prompt.md +5 -0
  331. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/cinematic_still_prompt.md +3 -0
  332. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/keyframe_prompt.md +6 -0
  333. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/location_reference_prompt.md +3 -0
  334. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/prop_prompt.md +3 -0
  335. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/tests/image_prompt_completeness_tests.md +5 -0
  336. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/SKILL.md +84 -0
  337. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/checklists/video_prompt_quality_checklist.md +9 -0
  338. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/examples/image_prompt_to_video_prompt.md +3 -0
  339. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/ai_film_production_pipeline.md +21 -0
  340. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/audio_motion_timing.md +16 -0
  341. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/camera_lens_movement.md +26 -0
  342. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/negative_prompt_patterns.md +18 -0
  343. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/prompt_anatomy.md +18 -0
  344. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/video_prompt_components.md +25 -0
  345. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/image_to_video_prompt.md +5 -0
  346. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/text_to_video_prompt.md +5 -0
  347. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/video_beat_prompt.md +9 -0
  348. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/tests/video_prompt_completeness_tests.md +5 -0
  349. package/resources/skills/director-skills/upstream/skills/model-adaptation/SKILL.md +82 -0
  350. package/resources/skills/director-skills/upstream/skills/model-adaptation/checklists/model_export_checklist.md +9 -0
  351. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/failed_model_transfer_diagnosis.md +5 -0
  352. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/image_prompt_translation.md +10 -0
  353. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/same_scene_across_models.md +9 -0
  354. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/universal_prompt_to_model_specific_exports.md +14 -0
  355. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/video_prompt_translation.md +9 -0
  356. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/failure_modes_by_model.md +13 -0
  357. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/image_model_profiles.md +226 -0
  358. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/model_capability_matrix.md +24 -0
  359. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/model_update_protocol.md +10 -0
  360. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/parameter_compatibility.md +20 -0
  361. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/prompt_translation_rules.md +18 -0
  362. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/video_model_profiles.md +251 -0
  363. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/model_comparison_table.md +4 -0
  364. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/model_specific_prompt_export.md +21 -0
  365. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/multi_model_export_package.md +18 -0
  366. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/prompt_translation_report.md +13 -0
  367. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/universal_prompt_brief.md +25 -0
  368. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/model_routing_tests.md +8 -0
  369. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/prompt_translation_tests.md +7 -0
  370. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/research_provenance_tests.md +6 -0
  371. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/unknown_capability_tests.md +7 -0
  372. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/SKILL.md +82 -0
  373. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/checklists/diagnostic_checklist.md +8 -0
  374. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/checklists/release_quality_checklist.md +9 -0
  375. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/examples/failed_output_to_revised_prompt.md +10 -0
  376. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/examples/final_prompt_package_export.md +10 -0
  377. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/failure_modes.md +13 -0
  378. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/iteration_diagnostics.md +17 -0
  379. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/revision_loops.md +17 -0
  380. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/story_voice_release_qc.md +30 -0
  381. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/failed_output_intake.md +13 -0
  382. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/final_production_package.md +11 -0
  383. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/prompt_revision_report.md +17 -0
  384. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/tests/prompt_quality_checklist_tests.md +6 -0
  385. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/SKILL.md +81 -0
  386. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/checklists/scene_quality_checklist.md +8 -0
  387. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/examples/scene_to_screenplay_excerpt.md +18 -0
  388. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/dialogue_and_subtext.md +15 -0
  389. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/fountain_workflow.md +29 -0
  390. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/scene_construction.md +20 -0
  391. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/screenplay_format.md +18 -0
  392. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/beat_sheet.md +6 -0
  393. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/dialogue_pass.md +9 -0
  394. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/fountain_scene.md +25 -0
  395. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/screenplay_scene.md +19 -0
  396. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/tests/screenplay_scene_tests.md +6 -0
  397. package/resources/skills/director-skills/upstream/skills/short-film-development/SKILL.md +83 -0
  398. package/resources/skills/director-skills/upstream/skills/short-film-development/checklists/concept_quality_checklist.md +8 -0
  399. package/resources/skills/director-skills/upstream/skills/short-film-development/examples/idea_to_logline.md +13 -0
  400. package/resources/skills/director-skills/upstream/skills/short-film-development/examples/logline_to_outline.md +10 -0
  401. package/resources/skills/director-skills/upstream/skills/short-film-development/references/ai_storytelling_workflow.md +46 -0
  402. package/resources/skills/director-skills/upstream/skills/short-film-development/references/continuity_handoff.md +8 -0
  403. package/resources/skills/director-skills/upstream/skills/short-film-development/references/screenplay_structure.md +19 -0
  404. package/resources/skills/director-skills/upstream/skills/short-film-development/references/story_development_workflow.md +23 -0
  405. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/beat_sheet.md +7 -0
  406. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/idea_intake.md +16 -0
  407. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/logline.md +10 -0
  408. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/short_film_treatment.md +23 -0
  409. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/short_story_plan.md +22 -0
  410. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/story_premise.md +19 -0
  411. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/story_revision_audit.md +22 -0
  412. package/resources/skills/director-skills/upstream/skills/short-film-development/tests/output_format_tests.md +3 -0
  413. package/resources/skills/director-skills/upstream/skills/short-film-development/tests/routing_tests.md +7 -0
  414. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/SKILL.md +77 -0
  415. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/checklists/shotlist_quality_checklist.md +8 -0
  416. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/examples/screenplay_scene_to_shot_list.md +8 -0
  417. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/camera_plan.md +11 -0
  418. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/production_breakdown.md +11 -0
  419. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/shotlist_schema.md +21 -0
  420. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/asset_list.md +7 -0
  421. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/camera_plan.md +10 -0
  422. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/scene_breakdown.md +16 -0
  423. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/shot_list.md +5 -0
  424. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/tests/screenplay_to_shotlist_tests.md +5 -0
  425. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/SKILL.md +77 -0
  426. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/checklists/style_quality_checklist.md +8 -0
  427. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/examples/genre_to_visual_direction.md +15 -0
  428. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/camera_lens_movement.md +26 -0
  429. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/cinematic_language.md +32 -0
  430. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/genre_style_tone.md +17 -0
  431. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/lighting_and_photorealism.md +19 -0
  432. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/templates/visual_style_bible.md +33 -0
  433. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/tests/style_translation_tests.md +5 -0
  434. package/resources/skills/director-skills/upstream/tests/master_router_tests.md +80 -0
  435. package/resources/skills/drama-production/manifest.json +933 -0
  436. package/resources/skills/drama-production/upstream/LICENSE +21 -0
  437. package/resources/skills/drama-production/upstream/skills/short-drama/SKILL.md +153 -0
  438. package/resources/skills/drama-production/upstream/skills/short-drama/agents/openai.yaml +4 -0
  439. package/resources/skills/drama-production/upstream/skills/short-drama/assets/creator-decision.example.jsonl +4 -0
  440. package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/app.js +1263 -0
  441. package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/index.html +114 -0
  442. package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/styles.css +297 -0
  443. package/resources/skills/drama-production/upstream/skills/short-drama/assets/production-observation.example.jsonl +2 -0
  444. package/resources/skills/drama-production/upstream/skills/short-drama/assets/project-template/short-drama.json +36 -0
  445. package/resources/skills/drama-production/upstream/skills/short-drama/assets/reference-observation.example.jsonl +2 -0
  446. package/resources/skills/drama-production/upstream/skills/short-drama/references/audience-reveal.md +21 -0
  447. package/resources/skills/drama-production/upstream/skills/short-drama/references/contract-and-ownership.md +45 -0
  448. package/resources/skills/drama-production/upstream/skills/short-drama/references/creator-documents.md +230 -0
  449. package/resources/skills/drama-production/upstream/skills/short-drama/references/creator-workflow.md +98 -0
  450. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards/Q/347/211/210/350/241/250/350/276/276.md +73 -0
  451. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//344/272/214/347/273/264/345/212/250/346/200/201/346/274/253.md +74 -0
  452. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//345/233/275/346/274/253/344/272/214/346/254/241/345/205/203.md +79 -0
  453. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//345/256/236/346/213/215.md +75 -0
  454. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//346/260/264/345/242/250/347/254/224/350/247/246.md +85 -0
  455. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//351/243/216/346/240/274/345/214/226/344/270/211/347/273/264.md +72 -0
  456. package/resources/skills/drama-production/upstream/skills/short-drama/references/knowhow-index.md +85 -0
  457. package/resources/skills/drama-production/upstream/skills/short-drama/references/look-development.md +51 -0
  458. package/resources/skills/drama-production/upstream/skills/short-drama/references/pickup-and-alternate.md +24 -0
  459. package/resources/skills/drama-production/upstream/skills/short-drama/references/production-form-profiles.md +126 -0
  460. package/resources/skills/drama-production/upstream/skills/short-drama/references/reference-roles.md +53 -0
  461. package/resources/skills/drama-production/upstream/skills/short-drama/references/runtime-preflight.md +24 -0
  462. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/creator_markdown_check.py +1413 -0
  463. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/dashboard_server.py +1798 -0
  464. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/project_tool.py +2454 -0
  465. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/selftest.py +287 -0
  466. package/resources/skills/drama-production/upstream/skills/short-drama-assets/SKILL.md +76 -0
  467. package/resources/skills/drama-production/upstream/skills/short-drama-assets/agents/openai.yaml +4 -0
  468. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/character-look.example.jsonl +3 -0
  469. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/continuity.example.jsonl +4 -0
  470. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/decisions.example.jsonl +5 -0
  471. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/location-view.example.jsonl +3 -0
  472. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/occurrences.example.jsonl +5 -0
  473. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/prop-state.example.jsonl +3 -0
  474. package/resources/skills/drama-production/upstream/skills/short-drama-assets/examples/minimal/characters.jsonl +2 -0
  475. package/resources/skills/drama-production/upstream/skills/short-drama-assets/examples/minimal/looks.jsonl +2 -0
  476. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/asset-review-checklist.md +82 -0
  477. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/character-and-look.md +190 -0
  478. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/continuity-delta.md +120 -0
  479. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/continuity-lock.md +122 -0
  480. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/identity-vs-variant.md +140 -0
  481. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/location-and-view.md +84 -0
  482. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/occurrence-extraction.md +108 -0
  483. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/prop-and-state.md +126 -0
  484. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/stage-contract.md +43 -0
  485. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/voice-direction.md +179 -0
  486. package/resources/skills/drama-production/upstream/skills/short-drama-assets/scripts/asset_check.py +267 -0
  487. package/resources/skills/drama-production/upstream/skills/short-drama-assets/scripts/selftest.py +89 -0
  488. package/resources/skills/drama-production/upstream/skills/short-drama-develop/SKILL.md +157 -0
  489. package/resources/skills/drama-production/upstream/skills/short-drama-develop/agents/openai.yaml +4 -0
  490. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -0
  491. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/creative-brief.md +120 -0
  492. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/episode-map.jsonl +1 -0
  493. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/story-engine.md +181 -0
  494. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/adaptation-craft.md +219 -0
  495. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/creative-reference-intake.md +57 -0
  496. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/director-brief-craft.md +113 -0
  497. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/episode-design.md +509 -0
  498. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-and-hook-playbook.md +240 -0
  499. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//344/272/262/345/255/220/351/232/220/347/247/230.md +112 -0
  500. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//344/273/231/344/276/240/344/277/256/347/234/237.md +51 -0
  501. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/212/250/344/275/234/344/273/273/345/212/241.md +51 -0
  502. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/217/244/350/243/205/346/235/203/350/260/213.md +51 -0
  503. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +51 -0
  504. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/256/266/345/272/255/345/205/263/347/263/273.md +51 -0
  505. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//346/202/254/347/226/221/350/247/204/345/210/231.md +51 -0
  506. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//347/224/237/346/264/273/346/265/201.md +51 -0
  507. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//347/240/264/351/225/234/351/207/215/345/234/206.md +109 -0
  508. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//350/201/214/345/234/272/345/226/234/345/211/247.md +51 -0
  509. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//350/261/252/351/227/250/345/251/232/346/201/213.md +51 -0
  510. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//350/272/253/344/273/275/351/224/231/344/275/215.md +51 -0
  511. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards.md +92 -0
  512. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/mechanism-loop.md +282 -0
  513. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/multi-episode-intake.md +128 -0
  514. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/premise-devices.md +211 -0
  515. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/reveal-reversal-payoff.md +99 -0
  516. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/serial-character-and-memory.md +293 -0
  517. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/stage-contract.md +88 -0
  518. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/story-craft.md +359 -0
  519. package/resources/skills/drama-production/upstream/skills/short-drama-develop/scripts/episode_intake.py +699 -0
  520. package/resources/skills/drama-production/upstream/skills/short-drama-develop/scripts/selftest.py +47 -0
  521. package/resources/skills/drama-production/upstream/skills/short-drama-edit/SKILL.md +146 -0
  522. package/resources/skills/drama-production/upstream/skills/short-drama-edit/agents/openai.yaml +4 -0
  523. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/README.md +60 -0
  524. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/package.json +15 -0
  525. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  526. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  527. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  528. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  529. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  530. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  531. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  532. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  533. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/cut-craft.md +38 -0
  534. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/delivery-verify.md +29 -0
  535. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/generated-footage.md +54 -0
  536. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
  537. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/stage-contract.md +51 -0
  538. package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
  539. package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/selftest.py +520 -0
  540. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/SKILL.md +73 -0
  541. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/agents/openai.yaml +4 -0
  542. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/image-prompt-spec.jsonl.md +187 -0
  543. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/image-prompts.md +42 -0
  544. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/lookdev-frame-spec.jsonl.md +128 -0
  545. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/lookdev-prompts.md +34 -0
  546. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +2 -0
  547. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/character-and-look.md +153 -0
  548. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/common-recipe.md +241 -0
  549. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/edit-and-revision.md +110 -0
  550. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/location-plate.md +151 -0
  551. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/look-and-state-variant.md +97 -0
  552. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/lookdev-frame.md +34 -0
  553. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/production-sheet-recipes.md +177 -0
  554. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/prop-plate.md +137 -0
  555. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/review-and-fixtures.md +92 -0
  556. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/stage-contract.md +32 -0
  557. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/scripts/image_prompt_check.py +443 -0
  558. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/scripts/selftest.py +95 -0
  559. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/SKILL.md +245 -0
  560. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/agents/openai.yaml +4 -0
  561. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +3 -0
  562. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/adaptation-triage.md +116 -0
  563. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/adaptation-value.md +124 -0
  564. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +155 -0
  565. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/chapter-extraction.md +144 -0
  566. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/stage-contract.md +67 -0
  567. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/scripts/novel_index.py +651 -0
  568. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/scripts/selftest.py +51 -0
  569. package/resources/skills/drama-production/upstream/skills/short-drama-produce/SKILL.md +183 -0
  570. package/resources/skills/drama-production/upstream/skills/short-drama-produce/agents/openai.yaml +4 -0
  571. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/adapter-contract.md +219 -0
  572. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/gpt-image-2.md +24 -0
  573. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-h3-video.md +86 -0
  574. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-music.md +25 -0
  575. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  576. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/seedance.md +64 -0
  577. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/fixture_adapter.py +77 -0
  578. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/production_tool.py +2205 -0
  579. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/provider_adapters.py +1747 -0
  580. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/selftest.py +135 -0
  581. package/resources/skills/drama-production/upstream/skills/short-drama-review/SKILL.md +118 -0
  582. package/resources/skills/drama-production/upstream/skills/short-drama-review/agents/openai.yaml +4 -0
  583. package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/finding-template.jsonl +2 -0
  584. package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/supersession-decision.example.json +42 -0
  585. package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/verdict-template.json +32 -0
  586. package/resources/skills/drama-production/upstream/skills/short-drama-review/examples/minimal-findings.jsonl +2 -0
  587. package/resources/skills/drama-production/upstream/skills/short-drama-review/examples/minimal-verdict.json +34 -0
  588. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/anti-template-repair.md +109 -0
  589. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/production-quality-gates.md +276 -0
  590. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/project-calibration.md +83 -0
  591. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/review-method.md +81 -0
  592. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-assets-prompts.md +111 -0
  593. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-source-analysis.md +108 -0
  594. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-story-script.md +264 -0
  595. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-visual-motion.md +184 -0
  596. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/stage-contract.md +28 -0
  597. package/resources/skills/drama-production/upstream/skills/short-drama-review/scripts/review_check.py +325 -0
  598. package/resources/skills/drama-production/upstream/skills/short-drama-review/scripts/selftest.py +87 -0
  599. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/SKILL.md +175 -0
  600. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/agents/openai.yaml +4 -0
  601. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +2 -0
  602. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/coverage-template.json +49 -0
  603. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/keyframe-prompts.md +36 -0
  604. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/keyframe-template.jsonl +2 -0
  605. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +12 -0
  606. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +2 -0
  607. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/shot-template.jsonl +2 -0
  608. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/blocking-playbooks.md +216 -0
  609. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
  610. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/coverage-audition.md +34 -0
  611. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/keyframe-craft.md +174 -0
  612. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  613. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/production-shot-grammar.md +279 -0
  614. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/review-and-fixtures.md +36 -0
  615. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/scene-visual-plan.md +30 -0
  616. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +155 -0
  617. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/shot-craft.md +266 -0
  618. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/shot-revision-identity.md +25 -0
  619. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/stage-contract.md +57 -0
  620. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/scripts/selftest.py +191 -0
  621. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/scripts/storyboard_check.py +824 -0
  622. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/SKILL.md +154 -0
  623. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/agents/openai.yaml +4 -0
  624. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +21 -0
  625. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/delivery-container.jsonl.md +101 -0
  626. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +217 -0
  627. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +2 -0
  628. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/music-spec.jsonl.md +31 -0
  629. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/performance.fragment.json +38 -0
  630. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/video-prompts.md +67 -0
  631. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +2 -0
  632. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/camera-audio-continuity.md +259 -0
  633. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/delivery-profile.md +273 -0
  634. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/generability.md +93 -0
  635. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/minimax-h3.md +253 -0
  636. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/motion-recipe.md +315 -0
  637. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/performance-action-timing.md +209 -0
  638. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/production-prompt-grammar.md +304 -0
  639. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/review-and-fixtures.md +139 -0
  640. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/seedance-2.0.md +50 -0
  641. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/seedance-2.5.md +122 -0
  642. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/stage-contract.md +60 -0
  643. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/target-model-profile.md +116 -0
  644. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/container_check.py +329 -0
  645. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/motion_timing_check.py +450 -0
  646. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/music_spec_check.py +327 -0
  647. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/selftest.py +82 -0
  648. package/resources/skills/drama-production/upstream/skills/short-drama-write/SKILL.md +99 -0
  649. package/resources/skills/drama-production/upstream/skills/short-drama-write/agents/openai.yaml +4 -0
  650. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/beats.jsonl +2 -0
  651. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/episode-card-standalone.json +120 -0
  652. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/episode-card.json +55 -0
  653. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/screenplay.md +36 -0
  654. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/voice-record-sheet.jsonl.md +89 -0
  655. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/dialogue-craft.md +526 -0
  656. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/production-format-dialect.md +273 -0
  657. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/scene-handoff-capsule.md +25 -0
  658. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/scene-sound-dramaturgy.md +43 -0
  659. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/screenplay-format.md +266 -0
  660. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/script-craft.md +745 -0
  661. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/stage-contract.md +36 -0
  662. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/substitutable-realization.md +107 -0
  663. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/duration_estimate.py +309 -0
  664. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/screenplay_index.py +1091 -0
  665. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/selftest.py +229 -0
  666. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/voice_sheet_check.py +416 -0
  667. package/resources/skills/viora-canvas-production/manifest.json +23 -0
  668. package/resources/skills/viora-canvas-production/references/canvas-contract.md +50 -0
  669. package/resources/skills/viora-canvas-production/references/creative-production.md +50 -0
  670. package/resources/skills/viora-canvas-production/references/generation-delivery.md +36 -0
  671. package/resources/skills/viora-canvas-production/references/production-ledger.md +25 -0
@@ -0,0 +1,1413 @@
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
+ # The same slot grammar, but the locator is a document entry instead of a file.
43
+ # A creator who generates in an external tool never has the file inside the
44
+ # project, and the copyable body only ever needed 顺序 and 用途 -- the path is a
45
+ # production input, not a prompt input. PLAN says "this picture does not exist
46
+ # here; attach it yourself in this order", which is a different claim from
47
+ # 「已有图片」 and from 「待补」.
48
+ PLAN_RE = re.compile(
49
+ r"(PLAN-[A-Z0-9-]+)(顺序:([1-9]\d*))· "
50
+ r"((?:IMG|SHOT)-[A-Z0-9-]+)《([^》\n]+)》"
51
+ r"((?:用途:([^;)\n]+);)?控制:([^;)]+);不得控制:([^)]+))",
52
+ re.IGNORECASE,
53
+ )
54
+ # One picture answers one question. 「同一张图承担两个作用时分开说明」 in
55
+ # references/reference-roles.md is what makes a slot translatable into a
56
+ # provider role, so the vocabulary is closed rather than free prose.
57
+ REF_PURPOSES = (
58
+ "身份",
59
+ "造型状态",
60
+ "地理",
61
+ "构图",
62
+ "尺度",
63
+ "效果",
64
+ "起始帧",
65
+ "结束帧",
66
+ "风格",
67
+ )
68
+ # `EP001-SC001` is the documented shape, but a project that scopes ids by season
69
+ # writes `S01-EP001-SC001`. Both are one stable scene id, so the pattern takes
70
+ # any hyphenated prefix rather than exactly one segment.
71
+ SLOT_HEAD_RE = re.compile(
72
+ r"(?:REF|PLAN)-[A-Za-z0-9][A-Za-z0-9-]*(顺序:", re.IGNORECASE
73
+ )
74
+ SCENE_ID_PATTERN = r"(?:[A-Za-z0-9]+-)+SC[0-9]+"
75
+ SCENE_HEADING_RE = re.compile(
76
+ r"^## (" + SCENE_ID_PATTERN + r")\b", re.MULTILINE
77
+ )
78
+ SCENE_ID_RE = re.compile(SCENE_ID_PATTERN)
79
+ # A scene the storyboard decided not to film. SHT-01 has always allowed the
80
+ # omission and always required a reason; without a place to write it the
81
+ # omission and an oversight look identical in the finished document.
82
+ UNFILMED_LINE_RE = re.compile(
83
+ r"^[ \t\u3000]*[-*+][ \t\u3000]*未拍场次[::](.*)$", re.MULTILINE
84
+ )
85
+ UNFILMED_ENTRY_RE = re.compile(
86
+ r"(" + SCENE_ID_PATTERN + r")(理由:([^)\n]+))"
87
+ )
88
+ # Dialogue reaches the model as a quoted run, either inside the H3 `<d>` tag or
89
+ # inside quotation marks. Runs shorter than four Han characters are in-frame
90
+ # labels and interjections far more often than they are lines, so the check
91
+ # stays off them rather than inventing an escape hatch for signage.
92
+ SPOKEN_SPAN_RE = re.compile(r"<d>([^<]*)</d>|[\"“]([^\"”]*)[\"”]|「([^」]*)」")
93
+ SPOKEN_RUN_RE = re.compile(r"[\u3400-\u9fff]{4,}")
94
+ EXPLICIT_TEXT_TO_VIDEO = "无(创作者已明确选择文生视频)"
95
+ PENDING_REFERENCE_SUFFIX_RE = re.compile(r";待补参考图:[^;。\n]+。?$")
96
+ VISUAL_CATEGORIES = ("人物", "造型", "地点", "道具")
97
+ # A malformed entry always tries to name something; a heading that is only the
98
+ # category word is a section divider ("## 人物"), and rejecting it left the
99
+ # document with no way to group entries at all.
100
+ VISUAL_SETTING_LINE_RE = re.compile(
101
+ r"^#{2,4}[ \t ]*(?:"
102
+ + "|".join(VISUAL_CATEGORIES)
103
+ + r")(?![ \t ]*$)[^\n]*$",
104
+ re.MULTILINE,
105
+ )
106
+ VISUAL_SETTING_HEADING_RE = re.compile(
107
+ r"^## (" + "|".join(VISUAL_CATEGORIES) + r") · (.+?)[ \t ]*$", re.MULTILINE
108
+ )
109
+ # Any other `## <word> · <name>` heading, so a 视觉依据 entry that fails to
110
+ # resolve can say "the entry is there, the section word is not one of the four"
111
+ # instead of blaming the storyboard for a `视觉设定.md` problem.
112
+ OTHER_SETTING_HEADING_RE = re.compile(r"^## ([^\n·]+?) · (.+?)[ \t ]*$", re.MULTILINE)
113
+ # 画面代称 is the one field the coverage check depends on, so — like a
114
+ # continuity lock — anything that looks like it has to parse rather than
115
+ # silently drop out.
116
+ # Anchored at the start of the line so a sentence that merely mentions the
117
+ # field is prose, not a malformed declaration.
118
+ SCREEN_NAME_LINE_RE = re.compile(
119
+ r"^[ \t ]*[-*+]?[ \t ]*画面代称[ \t ]*[::][^\n]*$", re.MULTILINE
120
+ )
121
+ SCREEN_NAME_RE = re.compile(
122
+ r"^[ \t ]*[-*+][ \t ]*画面代称[::](.+)$", re.MULTILINE
123
+ )
124
+ VISUAL_BASIS_PREFIX = "《视觉设定.md》·"
125
+ VISUAL_BASIS_ENTRY_RE = re.compile(
126
+ r"(" + "|".join(VISUAL_CATEGORIES) + r")「([^」\n]+)」(控制:([^)\n]+))"
127
+ )
128
+ # A subject a keyframe names but does not show — an owner's abandoned bag, a
129
+ # name on a screen, a homonym of an entry name. SHT-22 excludes these from
130
+ # coverage, so the field needs a way to say so instead of forcing the creator
131
+ # to declare an absent subject as present.
132
+ OFFSCREEN_PREFIX = ";画外:"
133
+ OFFSCREEN_ENTRY_RE = re.compile(
134
+ r"(" + "|".join(VISUAL_CATEGORIES) + r")「([^」\n]+)」"
135
+ )
136
+ # A declared lock must never become a no-op. Anything that *looks* like a lock
137
+ # line -- any list marker, any leading whitespace -- is captured here and then
138
+ # has to parse, so a creator who indents the bullet under 识别锚点 gets an error
139
+ # instead of silent non-enforcement.
140
+ LOCK_LINE_RE = re.compile(r"^[ \t\u3000]*[-*+][ \t\u3000]*连续性锁[::].*$", re.MULTILINE)
141
+ LOCK_RE = re.compile(
142
+ r"^[ \t\u3000]*[-*+][ \t\u3000]*连续性锁:(LOCK-[A-Z0-9-]+)《([^》\n]+)》"
143
+ r"(镜头:([^;)\n]+)"
144
+ r"(?:;图片提示词项:([^;)\n]+))?)"
145
+ r"· 锁面:(.+)$"
146
+ )
147
+ # The surface has to name what is in the picture. A match glued to a negation
148
+ # ("no pale blue sweater") describes what must be absent, so it cannot be the
149
+ # evidence that the fact is present.
150
+ # Chinese runs without spaces, so the CJK markers cannot require a preceding
151
+ # boundary the way the English ones do. A bare 无 is deliberately not a marker:
152
+ # 无袖毛衣 describes the garment rather than excluding it.
153
+ NEGATION_RE = re.compile(
154
+ r"(?:"
155
+ r"(?:^|[\s,;:(\[/—-])(?:no|not|non|never|without|avoid|excludes?|excluding|"
156
+ r"free\s+of|--?no)(?:\s+(?:a|an|the|any|some))?[\s-]*"
157
+ r"|(?:不要|不得|不能|不应|不含|不出现|没有|未|避免|禁止|排除|去掉|移除)"
158
+ r"(?:出现|包含|存在|带|有)?[\s]*"
159
+ r")$",
160
+ re.IGNORECASE,
161
+ )
162
+
163
+
164
+ def _sections(document: str, kind: str) -> dict[str, str]:
165
+ matches = [
166
+ match
167
+ for match in SECTION_RE.finditer(document)
168
+ if match.group(1).startswith(f"{kind}-")
169
+ ]
170
+ return {
171
+ match.group(1): document[
172
+ match.start() : matches[index + 1].start()
173
+ if index + 1 < len(matches)
174
+ else None
175
+ ]
176
+ for index, match in enumerate(matches)
177
+ }
178
+
179
+
180
+ def _fields(section: str, *, owner: str, errors: list[str]) -> dict[str, str]:
181
+ pairs = re.findall(r"^- ([^:\n]+):(.+)$", section, re.MULTILINE)
182
+ fields: dict[str, str] = {}
183
+ for key, value in pairs:
184
+ # `- **参考**:…` is ordinary Markdown and reads as the same field to a
185
+ # human. Taking the emphasis literally turned it into a different key,
186
+ # so the author got "缺少参考字段" while looking straight at 参考.
187
+ key = re.sub(r"^(?:\*\*|__|\*|_)(.+?)(?:\*\*|__|\*|_)$", r"\1", key.strip())
188
+ if key in fields:
189
+ errors.append(f"{owner}: 字段重复: {key}")
190
+ fields[key] = value
191
+ return fields
192
+
193
+
194
+ def _plain(value: str) -> str:
195
+ return value.strip().rstrip("。")
196
+
197
+
198
+ def _contains_ref_token(value: str) -> bool:
199
+ return "ref-" in value.casefold()
200
+
201
+
202
+ def _contains_plan_token(value: str) -> bool:
203
+ return "plan-" in value.casefold()
204
+
205
+
206
+ def _contains_slot_token(value: str) -> bool:
207
+ return _contains_ref_token(value) or _contains_plan_token(value)
208
+
209
+
210
+ def _is_none(value: str) -> bool:
211
+ # `[^)]+` would stop at the first closing bracket, so a gap list that names
212
+ # an entry in brackets -- 「江晨人物图(IMG-...)」 -- fell through to the REF
213
+ # syntax error, whose obvious repair is deleting the gap record. That is the
214
+ # silent downgrade this contract exists to prevent, so the note takes any
215
+ # content as long as it is bracketed.
216
+ return not _contains_slot_token(value) and bool(
217
+ re.fullmatch(r"无(?:(.+))?", _plain(value), re.DOTALL)
218
+ )
219
+
220
+
221
+ def _is_explicit_text_to_video(value: str) -> bool:
222
+ return _plain(value) == EXPLICIT_TEXT_TO_VIDEO
223
+
224
+
225
+ def _has_pending_references(value: str) -> bool:
226
+ plain = _plain(value)
227
+ return plain.startswith("无(待补参考图:") or bool(
228
+ PENDING_REFERENCE_SUFFIX_RE.search(value.strip())
229
+ )
230
+
231
+
232
+ def _is_no_external_reference(value: str) -> bool:
233
+ return not _contains_slot_token(value) and bool(
234
+ re.fullmatch(r"无(?:外部参考)?(?:;[^\n]*)?。?", value.strip())
235
+ )
236
+
237
+
238
+ def _copyable_prompt(
239
+ section: str, heading: str = r"可复制(?:通用)?提示词"
240
+ ) -> Optional[str]:
241
+ markers = list(re.finditer(rf"^### {heading}\s*$", section, re.MULTILINE))
242
+ if len(markers) != 1:
243
+ return None
244
+ body = section[markers[0].end() :]
245
+ following = re.search(r"^###\s+|^##\s+", body, re.MULTILINE)
246
+ if following is not None:
247
+ body = body[: following.start()]
248
+ lines = [line for line in body.splitlines() if line.strip()]
249
+ if not lines or any(not line.startswith(">") for line in lines):
250
+ return None
251
+ prompt = "\n".join(line[1:].lstrip() for line in lines).strip()
252
+ return prompt or None
253
+
254
+
255
+ def _copyable_prompt_cause(
256
+ section: str, heading: str = r"可复制(?:通用)?提示词"
257
+ ) -> str:
258
+ """Why `_copyable_prompt` returned None, as a suffix for the error.
259
+
260
+ "缺少唯一且非空的可复制提示词" is true of four different documents and tells
261
+ the author nothing about which one they wrote. The expensive case is a
262
+ single stray line -- a separator, an HTML comment, a note -- inside an
263
+ otherwise complete block: the prompt is visibly right there, so the message
264
+ reads as a checker bug and the real line goes unlooked at.
265
+ """
266
+
267
+ # Name the heading the author actually has to write, not this one's default:
268
+ # the keyframe caller passes a different one, and pointing at the wrong
269
+ # heading is worse than saying nothing.
270
+ label = "### " + re.sub(r"\(\?:([^)]*)\)\?", "", heading).replace("\\", "")
271
+ markers = list(re.finditer(rf"^### {heading}\s*$", section, re.MULTILINE))
272
+ if not markers:
273
+ return f":没有 `{label}` 小节标题"
274
+ if len(markers) > 1:
275
+ return f":`{label}` 出现了 {len(markers)} 次,只能有一个"
276
+ body = section[markers[0].end() :]
277
+ following = re.search(r"^###\s+|^##\s+", body, re.MULTILINE)
278
+ if following is not None:
279
+ body = body[: following.start()]
280
+ lines = [line for line in body.splitlines() if line.strip()]
281
+ if not lines:
282
+ return f":`{label}` 下面是空的"
283
+ intruders = [line for line in lines if not line.startswith(">")]
284
+ if intruders:
285
+ return (
286
+ ":小节内有不以 `>` 开头的行,整块因此不算引用块——"
287
+ f"把它移到小节外,或删掉:{_excerpt(intruders[0])}"
288
+ )
289
+ return ":引用块里只有空白"
290
+
291
+
292
+ def _portable_path(value: str) -> bool:
293
+ if not value or "\\" in value or re.match(r"^[A-Za-z]:", value):
294
+ return False
295
+ parts = value.split("/")
296
+ return not PurePosixPath(value).is_absolute() and not any(
297
+ part in {"", ".", ".."} for part in parts
298
+ )
299
+
300
+
301
+ def _inside(path: Path, root: Path) -> bool:
302
+ resolved = path.resolve()
303
+ return resolved == root or root in resolved.parents
304
+
305
+
306
+ class EntryIndex(NamedTuple):
307
+ """The document entries a `PLAN-...` locator is allowed to name."""
308
+
309
+ images: dict[str, str]
310
+ shots: frozenset
311
+
312
+
313
+ def _slot_matches(value: str) -> list:
314
+ """Every `REF-...` and `PLAN-...` slot in `value`, in written order.
315
+
316
+ The two patterns cannot match the same span: a REF locator has to be an
317
+ image path and a PLAN locator has to be an `IMG-`/`SHOT-` id.
318
+ """
319
+ found = []
320
+ for kind, pattern in (("REF", REF_RE), ("PLAN", PLAN_RE)):
321
+ for match in pattern.finditer(value):
322
+ found.append((match.start(), match.end(), kind, match.groups()))
323
+ found.sort(key=lambda item: item[0])
324
+ return found
325
+
326
+
327
+ def _references(
328
+ value: str,
329
+ owner: str,
330
+ project_root: Path,
331
+ errors: list[str],
332
+ entries: Optional[EntryIndex] = None,
333
+ ) -> frozenset:
334
+ """Validate one 输入参考图/参考 declaration; return the slot kinds it uses."""
335
+ if _is_none(value):
336
+ return frozenset()
337
+ reference_value = PENDING_REFERENCE_SUFFIX_RE.sub("", value.strip())
338
+ matches = _slot_matches(reference_value)
339
+ cursor = 0
340
+ separators_are_valid = True
341
+ for index, (start, end, _kind, _groups) in enumerate(matches):
342
+ if reference_value[cursor:start] != ("" if index == 0 else ";"):
343
+ separators_are_valid = False
344
+ cursor = end
345
+ trailing = reference_value[cursor:]
346
+ # Counted as slot heads on the same text the slots were matched in. A bare
347
+ # token count would read the `ref-` in a real path like
348
+ # `输入/参考图/ref-江晨.png` as a second slot and reject a correct declaration.
349
+ declared = len(SLOT_HEAD_RE.findall(reference_value))
350
+ kinds = frozenset(item[2] for item in matches)
351
+ if (
352
+ len(matches) != declared
353
+ or not matches
354
+ or not separators_are_valid
355
+ or trailing not in {"", "。"}
356
+ ):
357
+ # A gap list joined with ; reads as a fourth REF slot and would otherwise
358
+ # be reported as broken REF syntax, whose obvious repair is deleting the
359
+ # gap record -- the silent downgrade this contract exists to prevent.
360
+ if "待补参考图" in value and not _has_pending_references(value):
361
+ errors.append(
362
+ f"{owner}: 待补参考图必须写在最后一个 REF 槽位之后,"
363
+ "缺口之间只用、分隔"
364
+ )
365
+ elif _contains_plan_token(value) and not _contains_ref_token(value):
366
+ errors.append(f"{owner}: 输入参考图必须使用完整 PLAN 语法")
367
+ else:
368
+ errors.append(f"{owner}: 输入参考图必须使用完整 REF 语法")
369
+ return kinds
370
+ groups = [item[3] for item in matches]
371
+ slot_kinds = [item[2] for item in matches]
372
+ slots = [group[0] for group in groups]
373
+ orders = [int(group[1]) for group in groups]
374
+ locators = [group[2] for group in groups]
375
+ purposes = [(group[4] or "").strip() for group in groups]
376
+ # A declaration made only of PLAN slots is not a REF declaration, so its
377
+ # diagnostics must not send the creator looking for a REF line.
378
+ noun = "PLAN" if kinds == frozenset({"PLAN"}) else "REF"
379
+ if len(slots) != len(set(slots)):
380
+ errors.append(f"{owner}: {noun} 槽位重复")
381
+ if len(orders) != len(set(orders)) or sorted(orders) != list(
382
+ range(1, len(orders) + 1)
383
+ ):
384
+ errors.append(f"{owner}: {noun} 顺序必须唯一且从 1 连续编号")
385
+ locator_purposes = list(zip(locators, purposes))
386
+ if len(locator_purposes) != len(set(locator_purposes)):
387
+ errors.append(f"{owner}: {noun} 路径与用途完全重复")
388
+ for slot, purpose in zip(slots, purposes):
389
+ if not purpose:
390
+ errors.append(
391
+ f"{owner}: {noun} 缺少用途: {slot};用途只能是"
392
+ f"{'、'.join(REF_PURPOSES)}其中一个"
393
+ )
394
+ elif purpose not in REF_PURPOSES:
395
+ errors.append(
396
+ f"{owner}: {noun} 用途不在允许集合内: {slot}({purpose});"
397
+ f"只能是{'、'.join(REF_PURPOSES)}"
398
+ )
399
+ for role in ("起始帧", "结束帧"):
400
+ if purposes.count(role) > 1:
401
+ errors.append(f"{owner}: 同一条目只能有一张{role}参考图")
402
+ if "结束帧" in purposes and "起始帧" not in purposes:
403
+ errors.append(f"{owner}: 绑定结束帧参考图时必须同时绑定起始帧")
404
+ for index, (kind, group) in enumerate(zip(slot_kinds, groups)):
405
+ slot, raw_locator, label = group[0], group[2], group[3]
406
+ may_control, must_not_control = group[5], group[6]
407
+ if kind == "REF":
408
+ if not _portable_path(raw_locator):
409
+ errors.append(
410
+ f"{owner}: REF 路径不是安全的项目相对路径: {raw_locator}"
411
+ )
412
+ else:
413
+ reference_path = project_root / raw_locator
414
+ if not _inside(reference_path, project_root):
415
+ errors.append(f"{owner}: REF 路径越出项目根目录: {raw_locator}")
416
+ elif not reference_path.is_file():
417
+ errors.append(f"{owner}: REF 文件不存在: {raw_locator}")
418
+ else:
419
+ _plan_locator(
420
+ raw_locator, label, owner, slot, purposes[index], entries, errors
421
+ )
422
+ if not re.search(r"[\u4e00-\u9fff]", label):
423
+ errors.append(f"{owner}: {kind} 缺少中文名称: {slot}")
424
+ if not may_control.strip() or not must_not_control.strip():
425
+ errors.append(f"{owner}: {kind} 必须同时声明控制与不得控制: {slot}")
426
+ allowed = {item.strip() for item in re.split(r"[、,,]", may_control)}
427
+ prohibited = {item.strip() for item in re.split(r"[、,,]", must_not_control)}
428
+ if "" in allowed or "" in prohibited or allowed & prohibited:
429
+ errors.append(f"{owner}: {kind} 控制与不得控制范围冲突: {slot}")
430
+ return kinds
431
+
432
+
433
+ def _plan_locator(
434
+ locator: str,
435
+ label: str,
436
+ owner: str,
437
+ slot: str,
438
+ purpose: str,
439
+ entries: Optional[EntryIndex],
440
+ errors: list[str],
441
+ ) -> None:
442
+ """A planned picture still has to name an entry that exists today.
443
+
444
+ The picture itself is the creator's to supply, but what it depicts is
445
+ already decided by an `IMG-...` board or a `SHOT-...` frozen keyframe. A
446
+ locator that resolves to neither is a reference nobody can act on.
447
+ """
448
+ if entries is None:
449
+ return
450
+ if locator.startswith("IMG-"):
451
+ if locator not in entries.images:
452
+ errors.append(f"{owner}: PLAN 指向不存在的 IMG 条目: {locator}")
453
+ elif label != entries.images[locator]:
454
+ errors.append(f"{owner}: PLAN 中文名称与 IMG 标题不一致: {slot}")
455
+ elif locator.startswith("SHOT-"):
456
+ if locator not in entries.shots:
457
+ errors.append(f"{owner}: PLAN 指向不存在的 SHOT 条目: {locator}")
458
+ elif purpose == "起始帧":
459
+ # The picture that opens this shot is this shot's own frozen
460
+ # keyframe. Another shot's keyframe opens a different moment.
461
+ own = re.sub(r"^(?:SHOT|MOTION)-", "SHOT-", owner)
462
+ if own.startswith("SHOT-") and locator != own:
463
+ errors.append(
464
+ f"{owner}: 起始帧 PLAN 必须指向本镜的冻结关键帧: {locator}"
465
+ )
466
+ else:
467
+ errors.append(
468
+ f"{owner}: PLAN 定位符只能是 IMG-... 或 SHOT-...: {locator}"
469
+ )
470
+
471
+
472
+ def _excerpt(value: str, limit: int = 60) -> str:
473
+ """A short, single-line quote of an offending line for a diagnostic."""
474
+ collapsed = re.sub(r"\s+", " ", value).strip()
475
+ return collapsed if len(collapsed) <= limit else collapsed[: limit - 1] + "…"
476
+
477
+
478
+ def _unique(values: "Iterable[str]") -> list[str]:
479
+ seen: set[str] = set()
480
+ result: list[str] = []
481
+ for value in values:
482
+ if value not in seen:
483
+ seen.add(value)
484
+ result.append(value)
485
+ return result
486
+
487
+
488
+ def _normalized(value: str) -> str:
489
+ """Fold case and collapse whitespace so a hard-wrapped prompt still matches.
490
+
491
+ A copyable prompt is one rendered paragraph; the line breaks the repo's
492
+ Markdown style puts in it are not part of the text a creator wrote.
493
+ """
494
+ return re.sub(r"\s+", " ", value).strip().casefold()
495
+
496
+
497
+ def _wordish(character: str) -> bool:
498
+ return bool(character) and character.isascii() and (
499
+ character.isalnum() or character == "-"
500
+ )
501
+
502
+
503
+ def _carries_surface(prompt: str, surface: str) -> bool:
504
+ """Is this lock surface actually asserted by this prompt?
505
+
506
+ Plain containment is not enough. `chipped white enamel mug` must not be
507
+ satisfied by `unchipped ... mug`, and `no pale blue sweater` in a negative
508
+ tail asserts the opposite of the fact the lock exists to hold.
509
+ """
510
+ haystack = _normalized(prompt)
511
+ needle = _normalized(surface)
512
+ if not needle:
513
+ return False
514
+ start = haystack.find(needle)
515
+ while start != -1:
516
+ end = start + len(needle)
517
+ before = haystack[start - 1] if start else ""
518
+ after = haystack[end] if end < len(haystack) else ""
519
+ # Only ASCII words have boundaries to glue across. Chinese is written
520
+ # without spaces, so treating an adjacent CJK character as "glued" would
521
+ # make a Chinese surface impossible to satisfy.
522
+ glued = (
523
+ (_wordish(needle[:1]) and _wordish(before))
524
+ or (_wordish(needle[-1:]) and _wordish(after))
525
+ )
526
+ if not glued and NEGATION_RE.search(haystack[:start]) is None:
527
+ return True
528
+ start = haystack.find(needle, start + 1)
529
+ return False
530
+
531
+
532
+ class VisualEntry(NamedTuple):
533
+ """One `视觉设定.md` entry plus every name a prompt body may call it by."""
534
+
535
+ category: str
536
+ name: str
537
+ designators: list[str]
538
+
539
+ @property
540
+ def key(self) -> tuple[str, str]:
541
+ return (self.category, self.name)
542
+
543
+
544
+ def _visual_entries(document: str, errors: list[str]) -> list[VisualEntry]:
545
+ """Parse the 人物/造型/地点/道具 entries of one `视觉设定.md`.
546
+
547
+ A heading that *looks* like an entry but does not parse would silently drop
548
+ out of the coverage check and then reappear as "视觉依据 names an entry that
549
+ does not exist", so it is rejected here where the cause is visible.
550
+ """
551
+ for line in VISUAL_SETTING_LINE_RE.findall(document):
552
+ if VISUAL_SETTING_HEADING_RE.match(line) is None:
553
+ errors.append(
554
+ "视觉设定.md: 条目标题必须写成 `## <人物|造型|地点|道具> · <名称>`: "
555
+ + _excerpt(line)
556
+ )
557
+ for line in SCREEN_NAME_LINE_RE.findall(document):
558
+ if SCREEN_NAME_RE.match(line) is None:
559
+ errors.append(
560
+ "视觉设定.md: 画面代称必须写成 `- 画面代称:<正文里的拼写>`: "
561
+ + _excerpt(line)
562
+ )
563
+ headings = list(VISUAL_SETTING_HEADING_RE.finditer(document))
564
+ entries: list[VisualEntry] = []
565
+ seen: set[tuple[str, str]] = set()
566
+ for index, heading in enumerate(headings):
567
+ end = (
568
+ headings[index + 1].start()
569
+ if index + 1 < len(headings)
570
+ else len(document)
571
+ )
572
+ category, name = heading.group(1), heading.group(2).strip()
573
+ if not name:
574
+ errors.append(f"视觉设定.md: {category}条目缺少名称")
575
+ continue
576
+ if (category, name) in seen:
577
+ errors.append(f"视觉设定.md: 条目重复: {category}「{name}」")
578
+ continue
579
+ seen.add((category, name))
580
+ declared = SCREEN_NAME_RE.findall(document[heading.end() : end])
581
+ if not declared:
582
+ # No declaration: the entry's own name is the designator, which is
583
+ # what a prompt body written in the project's own language calls it.
584
+ designators = [name]
585
+ elif all(_is_none(_plain(raw)) for raw in declared):
586
+ # `画面代称:无` is the deliberate opt-out for a name too common to
587
+ # match reliably in prose (道具「手机」 against 手机店). It removes the
588
+ # derived name too, otherwise the opt-out would do nothing.
589
+ designators = []
590
+ else:
591
+ designators = []
592
+ for raw in declared:
593
+ value = _plain(raw)
594
+ if _is_none(value):
595
+ continue
596
+ for item in re.split(r"[、,,]", value):
597
+ item = item.strip()
598
+ if item and item not in designators:
599
+ designators.append(item)
600
+ # Only a *declared* designator earns this diagnostic: the creator chose a
601
+ # spelling the matcher cannot honour and deserves to know. A one-character
602
+ # entry name they never declared is simply not name-matched, the same as
603
+ # any entry in a project whose prompt body is written in another language.
604
+ if declared:
605
+ for designator in designators:
606
+ if len(designator.strip()) < 2:
607
+ errors.append(
608
+ f"视觉设定.md: {category}「{name}」的画面代称「{designator}」"
609
+ "过短,无法在正文中可靠识别;请写成至少两个字符,或写「画面代称:无」"
610
+ )
611
+ entries.append(VisualEntry(category, name, designators))
612
+ return entries
613
+
614
+
615
+ def _named_entries(
616
+ prompt: str,
617
+ entries: list[VisualEntry],
618
+ *,
619
+ fold_case: bool = False,
620
+ by_entry_name: bool = False,
621
+ ) -> set[tuple[str, str]]:
622
+ """Which declared entries does this frozen keyframe actually call by name?
623
+
624
+ Longer designators win: a keyframe that shows 「江晨手机」 names the prop, and
625
+ the 人物「江晨」 substring inside it is not a second, unrelated claim.
626
+ """
627
+ haystack = re.sub(r"\s+", " ", prompt).strip()
628
+ if fold_case:
629
+ haystack = haystack.casefold()
630
+ owners: dict[str, list[VisualEntry]] = {}
631
+ for entry in entries:
632
+ # 来源 quotes 剧本.md, which is written in the project's own language and
633
+ # calls people by their entry names — not by the 画面代称 a prompt body
634
+ # uses. Matching a Chinese screenplay quote against an English designator
635
+ # would never fire, so that text is matched by entry name instead. An
636
+ # entry that opted out with `画面代称:无` opted out of being found by
637
+ # name at all, and stays out of both.
638
+ needles = (
639
+ [entry.name] if by_entry_name and entry.designators else entry.designators
640
+ )
641
+ for designator in needles:
642
+ needle = re.sub(r"\s+", " ", designator).strip()
643
+ if fold_case:
644
+ needle = needle.casefold()
645
+ # A one-character designator matches far too much prose to be
646
+ # evidence; _visual_entries already reports it.
647
+ if len(needle) < 2:
648
+ continue
649
+ owners.setdefault(needle, []).append(entry)
650
+ named: set[tuple[str, str]] = set()
651
+ # Spans of every occurrence of a longer designator, whether or not it was
652
+ # credited. 「戒指盒」 occupies its characters even when the sentence is
653
+ # 「没有戒指」, so 道具「戒指」 must not be read out of it.
654
+ taken: list[tuple[int, int]] = []
655
+ for needle in sorted(owners, key=len, reverse=True):
656
+ occurrences: list[tuple[int, int]] = []
657
+ start = haystack.find(needle)
658
+ while start != -1:
659
+ end = start + len(needle)
660
+ occurrences.append((start, end))
661
+ start = haystack.find(needle, start + 1)
662
+ for start, end in occurrences:
663
+ before = haystack[start - 1] if start else ""
664
+ after = haystack[end] if end < len(haystack) else ""
665
+ glued = (
666
+ (_wordish(needle[:1]) and _wordish(before))
667
+ or (_wordish(needle[-1:]) and _wordish(after))
668
+ )
669
+ # Strictly longer, so two entries sharing one designator are both
670
+ # credited instead of the first in document order winning.
671
+ inside_longer = any(
672
+ taken_start <= start
673
+ and end <= taken_end
674
+ and taken_end - taken_start > end - start
675
+ for taken_start, taken_end in taken
676
+ )
677
+ if (
678
+ not glued
679
+ and not inside_longer
680
+ and NEGATION_RE.search(haystack[:start]) is None
681
+ ):
682
+ named.update(entry.key for entry in owners[needle])
683
+ taken.extend(occurrences)
684
+ return named
685
+
686
+
687
+ class VisualBasis(NamedTuple):
688
+ """One shot's parsed 视觉依据: what it declares, and what it excludes."""
689
+
690
+ parsed: bool
691
+ declared: set[tuple[str, str]]
692
+ offscreen: set[tuple[str, str]]
693
+
694
+ @property
695
+ def accounted(self) -> set[tuple[str, str]]:
696
+ return self.declared | self.offscreen
697
+
698
+
699
+ def _resolve_entry(
700
+ key: tuple[str, str],
701
+ known: set[tuple[str, str]],
702
+ other_headings: dict[str, str],
703
+ owner: str,
704
+ errors: list[str],
705
+ ) -> None:
706
+ if key in known:
707
+ return
708
+ category, name = key
709
+ section = other_headings.get(name)
710
+ if section is not None:
711
+ errors.append(
712
+ f"{owner}: 《视觉设定.md》里有「{name}」,但它的分节词是「{section}」;"
713
+ "条目标题必须写成 `## <人物|造型|地点|道具> · <名称>`"
714
+ )
715
+ else:
716
+ errors.append(
717
+ f"{owner}: 视觉依据指向不存在的《视觉设定.md》条目: {category}「{name}」"
718
+ )
719
+
720
+
721
+ def _check_named_coverage(
722
+ prompt: str,
723
+ owner: str,
724
+ where: str,
725
+ basis: "VisualBasis",
726
+ entries: list[VisualEntry],
727
+ errors: list[str],
728
+ *,
729
+ by_entry_name: bool = False,
730
+ ) -> None:
731
+ """Every entry this text calls by name is either in frame or declared 画外."""
732
+ # Matching is case-sensitive so an ordinary English word never impersonates a
733
+ # character called May or Will. A body that writes the name in another case
734
+ # would otherwise fall out of the check silently, so it is reported here.
735
+ named = _named_entries(prompt, entries, by_entry_name=by_entry_name)
736
+ folded = _named_entries(
737
+ prompt, entries, fold_case=True, by_entry_name=by_entry_name
738
+ )
739
+ for category, name in sorted(folded - named):
740
+ errors.append(
741
+ f"{owner}: {where}里的名字与画面代称大小写不一致: {category}「{name}」;"
742
+ "同一个名字全集只用一个拼写"
743
+ )
744
+ for category, name in sorted(named):
745
+ if (category, name) not in basis.accounted:
746
+ errors.append(
747
+ f"{owner}: {where}写到{category}「{name}」,视觉依据没有覆盖;"
748
+ "本镜确实看不见时在视觉依据末尾加「;画外:"
749
+ f"{category}「{name}」」,正文里这个名字不可靠时在《视觉设定.md》"
750
+ "写「画面代称:无」"
751
+ )
752
+
753
+
754
+ def _visual_basis(
755
+ value: str,
756
+ owner: str,
757
+ entries: list[VisualEntry],
758
+ other_headings: dict[str, str],
759
+ errors: list[str],
760
+ ) -> VisualBasis:
761
+ """Parse one shot's 视觉依据 and resolve it against `视觉设定.md`."""
762
+ empty = VisualBasis(True, set(), set())
763
+ if _is_none(value):
764
+ return empty
765
+ plain = _plain(value)
766
+ offscreen_raw = ""
767
+ if OFFSCREEN_PREFIX in plain:
768
+ plain, _, offscreen_raw = plain.partition(OFFSCREEN_PREFIX)
769
+ prefixed = plain.startswith(VISUAL_BASIS_PREFIX)
770
+ body = plain[len(VISUAL_BASIS_PREFIX) :] if prefixed else ""
771
+ matches = list(VISUAL_BASIS_ENTRY_RE.finditer(body)) if prefixed else []
772
+ cursor = 0
773
+ separators_are_valid = True
774
+ for index, match in enumerate(matches):
775
+ # Repeating 《视觉设定.md》· before each entry says the same thing and
776
+ # reads naturally; rejecting it would cost a round trip over nothing.
777
+ separator = body[cursor : match.start()]
778
+ allowed = (
779
+ {""}
780
+ if index == 0
781
+ else {";", ";" + VISUAL_BASIS_PREFIX}
782
+ )
783
+ if separator not in allowed:
784
+ separators_are_valid = False
785
+ cursor = match.end()
786
+ if not prefixed or not matches or not separators_are_valid or body[cursor:]:
787
+ errors.append(
788
+ f"{owner}: 视觉依据必须使用完整语法:"
789
+ "《视觉设定.md》·<人物|造型|地点|道具>「<名称>」(控制:<范围>),多项用;连接"
790
+ )
791
+ # Coverage is not reported on top of a parse failure: every entry would
792
+ # be listed as uncovered and bury the one error that matters.
793
+ return VisualBasis(False, set(), set())
794
+ known = {entry.key for entry in entries}
795
+ declared: set[tuple[str, str]] = set()
796
+ for match in matches:
797
+ key = (match.group(1), match.group(2).strip())
798
+ if key in declared:
799
+ errors.append(f"{owner}: 视觉依据条目重复: {key[0]}「{key[1]}」")
800
+ declared.add(key)
801
+ _resolve_entry(key, known, other_headings, owner, errors)
802
+ if not match.group(3).strip():
803
+ errors.append(f"{owner}: 视觉依据缺少控制范围: {key[0]}「{key[1]}」")
804
+ offscreen: set[tuple[str, str]] = set()
805
+ if offscreen_raw:
806
+ offscreen_raw = offscreen_raw.replace(VISUAL_BASIS_PREFIX, "")
807
+ remainder = OFFSCREEN_ENTRY_RE.sub("", offscreen_raw).strip(";、 ")
808
+ offscreen_matches = list(OFFSCREEN_ENTRY_RE.finditer(offscreen_raw))
809
+ if not offscreen_matches or remainder:
810
+ errors.append(
811
+ f"{owner}: 画外清单必须写成 `;画外:<人物|造型|地点|道具>「<名称>」`,多项用;连接"
812
+ )
813
+ return VisualBasis(False, set(), set())
814
+ for match in offscreen_matches:
815
+ key = (match.group(1), match.group(2).strip())
816
+ if key in declared:
817
+ errors.append(
818
+ f"{owner}: {key[0]}「{key[1]}」同时写进视觉依据和画外清单"
819
+ )
820
+ offscreen.add(key)
821
+ _resolve_entry(key, known, other_headings, owner, errors)
822
+ return VisualBasis(True, declared, offscreen)
823
+
824
+
825
+ class ContinuityLock(NamedTuple):
826
+ """One declared cross-shot lock: the exact surface and where it applies."""
827
+
828
+ lock_id: str
829
+ surface: str
830
+ shots: list[str]
831
+ images: list[str]
832
+
833
+
834
+ def _continuity_locks(document: str, errors: list[str]) -> list[ContinuityLock]:
835
+ """Parse the declared continuity locks of one 视觉设定.md."""
836
+ locks: list[ContinuityLock] = []
837
+ seen: set[str] = set()
838
+ for line in LOCK_LINE_RE.findall(document):
839
+ match = LOCK_RE.match(line)
840
+ if match is None:
841
+ errors.append(
842
+ "视觉设定.md: 连续性锁必须使用完整语法: " + _excerpt(line)
843
+ )
844
+ continue
845
+ lock_id, label, scope, image_scope, surface = match.groups()
846
+ if lock_id in seen:
847
+ errors.append(f"{lock_id}: 连续性锁 ID 重复")
848
+ continue
849
+ seen.add(lock_id)
850
+ if not re.search(r"[\u3400-\u9fff]", label):
851
+ errors.append(f"{lock_id}: 连续性锁缺少中文名称")
852
+ surface = _plain(surface)
853
+ if not surface:
854
+ errors.append(f"{lock_id}: 连续性锁缺少锁面")
855
+ continue
856
+ shots = _unique(
857
+ item.strip() for item in re.split(r"[、,,]", _plain(scope)) if item.strip()
858
+ )
859
+ if not shots:
860
+ errors.append(f"{lock_id}: 连续性锁缺少镜头范围")
861
+ continue
862
+ if "全集" in shots and len(shots) != 1:
863
+ errors.append(f"{lock_id}: 连续性锁的镜头范围不能把全集与具体镜头混写")
864
+ continue
865
+ images: list[str] = []
866
+ if image_scope is not None and not _is_none(image_scope):
867
+ images = _unique(
868
+ item.strip()
869
+ for item in re.split(r"[、,,]", _plain(image_scope))
870
+ if item.strip()
871
+ )
872
+ if any(not item.startswith("IMG-") for item in images):
873
+ errors.append(f"{lock_id}: 连续性锁的图片提示词项必须使用 IMG- ID")
874
+ continue
875
+ locks.append(ContinuityLock(lock_id, surface, shots, images))
876
+ return locks
877
+
878
+
879
+ def _prompt_language(project_root: Path) -> str:
880
+ """The language a copyable prompt body is written in.
881
+
882
+ Mirrors the skills' own routing: the project's declared prompt language,
883
+ falling back to `en` when there is no `short-drama.json` -- which is what
884
+ the storyboard skill tells the keyframe author to assume.
885
+ """
886
+ configuration = project_root / "short-drama.json"
887
+ if not configuration.is_file():
888
+ return "en"
889
+ try:
890
+ project = json.loads(configuration.read_text(encoding="utf-8"))
891
+ except (OSError, ValueError):
892
+ return "en"
893
+ if not isinstance(project, dict):
894
+ return "en"
895
+ authority = project.get("creator_authority")
896
+ profile = (
897
+ authority.get("production_profile") if isinstance(authority, dict) else None
898
+ )
899
+ if isinstance(profile, dict) and profile.get("status") == "accepted":
900
+ choices = profile.get("choices")
901
+ if isinstance(choices, dict):
902
+ declared = choices.get("video_prompt_language")
903
+ if isinstance(declared, str) and declared:
904
+ return declared
905
+ formats = project.get("format")
906
+ declared = formats.get("prompt_language") if isinstance(formats, dict) else None
907
+ return declared if isinstance(declared, str) and declared else "en"
908
+
909
+
910
+ def _check_language_designators(
911
+ project_root: Path, *, entries: list[VisualEntry], errors: list[str]
912
+ ) -> None:
913
+ """Every character must be nameable in the prompt body's language.
914
+
915
+ Without this, omitting `画面代称` is a silent opt-out of the coverage check
916
+ for exactly the projects that need it -- the keyframe body defaults to `en`
917
+ while `视觉设定.md` is Chinese, which is the shape issue #94 reported.
918
+
919
+ This deliberately does not wait until some shot's 视觉依据 references the
920
+ entry. Gating on that made the two halves of the contract land in different
921
+ rounds: a document missing 视觉依据 got one error, and only after fixing it
922
+ did the missing 画面代称 appear.
923
+ """
924
+ if _prompt_language(project_root).casefold().startswith("zh"):
925
+ return
926
+ for entry in entries:
927
+ if entry.category != "人物":
928
+ continue
929
+ if entry.designators == [entry.name]:
930
+ errors.append(
931
+ f"视觉设定.md: 人物「{entry.name}」缺少画面代称;"
932
+ "提示词正文不是中文时,写「画面代称:<正文里的拼写>」,"
933
+ "正文从不点名时写「画面代称:无」"
934
+ )
935
+
936
+
937
+ def _declared_seconds(value: str) -> Optional[float]:
938
+ """The number of seconds a 时长 field declares, however it is spelled.
939
+
940
+ `4s`, `4 秒` and `4秒` are the same duration. Returning None for anything
941
+ else keeps an unparseable value out of the comparison instead of turning a
942
+ formatting difference into a false duration conflict.
943
+ """
944
+ match = re.search(r"(\d+(?:\.\d+)?)", value or "")
945
+ return float(match.group(1)) if match else None
946
+
947
+
948
+ def _check_continuity_locks(
949
+ locks: list[ContinuityLock],
950
+ *,
951
+ shots: dict[str, str],
952
+ motion_by_shot: dict[str, tuple[str, str, Optional[str]]],
953
+ image_prompts: dict[str, Optional[str]],
954
+ errors: list[str],
955
+ ) -> None:
956
+ """Require every declared lock surface to be present where it was scoped."""
957
+ for lock in locks:
958
+ lock_id = lock.lock_id
959
+ surface = lock.surface
960
+ targets = sorted(shots) if lock.shots == ["全集"] else lock.shots
961
+ for shot_id in targets:
962
+ if shot_id not in shots:
963
+ errors.append(f"{lock_id}: 连续性锁指向不存在的镜头: {shot_id}")
964
+ continue
965
+ keyframe = _copyable_prompt(shots[shot_id], heading=r"冻结关键帧提示词")
966
+ if keyframe is None:
967
+ errors.append(f"{lock_id}: {shot_id} 缺少可读的冻结关键帧提示词")
968
+ elif not _carries_surface(keyframe, surface):
969
+ errors.append(f"{lock_id}: {shot_id} 冻结关键帧提示词缺少锁面")
970
+ motion = motion_by_shot.get(shot_id)
971
+ if motion is None:
972
+ continue
973
+ motion_id, _, copyable_prompt = motion
974
+ if copyable_prompt is not None and not _carries_surface(
975
+ copyable_prompt, surface
976
+ ):
977
+ errors.append(f"{lock_id}: {motion_id} 可复制提示词缺少锁面")
978
+ for image_id in lock.images:
979
+ if image_id not in image_prompts:
980
+ errors.append(f"{lock_id}: 连续性锁指向不存在的 IMG 条目: {image_id}")
981
+ continue
982
+ image_prompt = image_prompts[image_id]
983
+ if image_prompt is not None and not _carries_surface(image_prompt, surface):
984
+ errors.append(f"{lock_id}: {image_id} 可复制提示词缺少锁面")
985
+
986
+
987
+ def _storyboard_head(storyboard: str) -> str:
988
+ """The part of 分镜.md that belongs to the episode rather than to a shot."""
989
+ match = re.search(r"^## SHOT-", storyboard, re.MULTILINE)
990
+ return storyboard if match is None else storyboard[: match.start()]
991
+
992
+
993
+ def _shot_sources(
994
+ value: str, owner: str, scenes: "frozenset", errors: list[str]
995
+ ) -> list:
996
+ """Resolve one shot's 来源 against the scene ids that exist in 剧本.md."""
997
+ plain = _plain(value)
998
+ if not plain:
999
+ errors.append(f"{owner}: 缺少来源字段")
1000
+ return []
1001
+ # 《镜头手艺》 asks for the scene id plus a short quote of the source line,
1002
+ # and a quote contains commas of its own. So the ids are read out of the
1003
+ # field rather than by cutting it into pieces on punctuation.
1004
+ if SCENE_ID_RE.match(plain) is None:
1005
+ errors.append(
1006
+ f"{owner}: 来源必须以《剧本.md》的场景 ID 开头: {_excerpt(plain, 24)}"
1007
+ )
1008
+ return []
1009
+ resolved = []
1010
+ for scene_id in _unique(SCENE_ID_RE.findall(plain)):
1011
+ if scene_id not in scenes:
1012
+ errors.append(f"{owner}: 来源场景不在《剧本.md》中: {scene_id}")
1013
+ else:
1014
+ resolved.append(scene_id)
1015
+ return resolved
1016
+
1017
+
1018
+ def _unfilmed_scenes(
1019
+ storyboard: str, scenes: "frozenset", errors: list[str]
1020
+ ) -> dict:
1021
+ """Scenes 分镜.md declares it deliberately does not film, with reasons."""
1022
+ head = _storyboard_head(storyboard)
1023
+ declared: dict = {}
1024
+ # The line belongs to the episode, not to a shot. Placed inside a SHOT
1025
+ # section it would be read as that shot's field and quietly do nothing,
1026
+ # which looks exactly like forgetting to write it.
1027
+ if len(UNFILMED_LINE_RE.findall(storyboard)) != len(
1028
+ UNFILMED_LINE_RE.findall(head)
1029
+ ):
1030
+ errors.append("分镜.md: 未拍场次要写在第一个 SHOT 之前的正文开头")
1031
+ for value in UNFILMED_LINE_RE.findall(head):
1032
+ value = value.strip()
1033
+ entries = UNFILMED_ENTRY_RE.findall(value)
1034
+ remainder = UNFILMED_ENTRY_RE.sub("", value).strip(";;。 ")
1035
+ if not entries or remainder:
1036
+ errors.append(
1037
+ "分镜.md: 未拍场次必须写成 <场景 ID>(理由:……),多项用;分隔"
1038
+ )
1039
+ continue
1040
+ for scene_id, reason in entries:
1041
+ if not reason.strip():
1042
+ errors.append(f"分镜.md: 未拍场次缺少理由: {scene_id}")
1043
+ elif scene_id not in scenes:
1044
+ errors.append(f"分镜.md: 未拍场次不在《剧本.md》中: {scene_id}")
1045
+ else:
1046
+ declared[scene_id] = reason.strip()
1047
+ return declared
1048
+
1049
+
1050
+ def _spoken_key(value: str) -> str:
1051
+ """Drop punctuation and spacing so quoting choices do not decide identity."""
1052
+ return re.sub(r"[^\u3400-\u9fff0-9A-Za-z]", "", value)
1053
+
1054
+
1055
+ def _quoted_speech(prompt: str) -> list:
1056
+ # The body is one prompt; a quote that happens to wrap across lines is the
1057
+ # same quote. Chinese does not separate words with spaces, so rejoining the
1058
+ # lines reconstructs the run exactly.
1059
+ joined = prompt.replace("\n", "")
1060
+ runs: list = []
1061
+ for match in SPOKEN_SPAN_RE.finditer(joined):
1062
+ span = next((group for group in match.groups() if group), "")
1063
+ # An unpaired quotation mark can pair with one much later and swallow
1064
+ # the narration between them. That span is not a quotation, so it is
1065
+ # not evidence of an invented line either.
1066
+ if len(span) > 200:
1067
+ continue
1068
+ runs.extend(SPOKEN_RUN_RE.findall(span))
1069
+ return runs
1070
+
1071
+
1072
+ def _check_spoken_lines(
1073
+ prompt: str, owner: str, sources: "tuple", errors: list[str]
1074
+ ) -> None:
1075
+ """Quoted Chinese in a copyable body has to come from an upstream document.
1076
+
1077
+ The video stage already owes the screenplay verbatim dialogue. Checking it
1078
+ here is what stops a prompt from writing the line the shot wishes the
1079
+ character had said. 视觉设定.md and 分镜.md count as sources too: a location
1080
+ or prop entry carries the exact text on a sign, and a shot's own fields
1081
+ carry the on-screen text and sound this shot was designed around. What no
1082
+ upstream document contains is a line the prompt made up.
1083
+ """
1084
+ for run in _quoted_speech(prompt):
1085
+ key = _spoken_key(run)
1086
+ if key and not any(key in source for source in sources):
1087
+ errors.append(
1088
+ f"{owner}: 可复制提示词的引文在《剧本.md》《视觉设定.md》《分镜.md》里都找不到: "
1089
+ f"{_excerpt(run, 24)}"
1090
+ )
1091
+
1092
+
1093
+ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list[str]:
1094
+ """Return all deterministic contract errors for ``episode``."""
1095
+ episode = episode.resolve()
1096
+ project_root = (project_root or episode.parent.parent).resolve()
1097
+ errors: list[str] = []
1098
+ missing = [name for name in REQUIRED_DOCUMENTS if not (episode / name).is_file()]
1099
+ if missing:
1100
+ return [f"缺少创作文档: {', '.join(missing)}"]
1101
+
1102
+ images = (episode / "图片提示词.md").read_text(encoding="utf-8")
1103
+ storyboard = (episode / "分镜.md").read_text(encoding="utf-8")
1104
+ video = (episode / "视频提示词.md").read_text(encoding="utf-8")
1105
+ visual = (episode / "视觉设定.md").read_text(encoding="utf-8")
1106
+ screenplay = (episode / "剧本.md").read_text(encoding="utf-8")
1107
+ scene_headings = SCENE_HEADING_RE.findall(screenplay)
1108
+ scenes = frozenset(scene_headings)
1109
+ if not scenes:
1110
+ errors.append(
1111
+ "剧本.md: 没有可解析的场景标题;场景标题写成 ## <场景 ID> 内|外 · 地点 · 时间"
1112
+ )
1113
+ if len(scene_headings) != len(scenes):
1114
+ errors.append("剧本.md: 场景 ID 重复")
1115
+ spoken_sources = (
1116
+ _spoken_key(screenplay),
1117
+ _spoken_key(visual),
1118
+ _spoken_key(storyboard),
1119
+ )
1120
+ unfilmed = _unfilmed_scenes(storyboard, scenes, errors)
1121
+ claimed_scenes: set = set()
1122
+ locks = _continuity_locks(visual, errors)
1123
+ visual_entries = _visual_entries(visual, errors)
1124
+ other_headings = {
1125
+ match.group(2).strip(): match.group(1).strip()
1126
+ for match in OTHER_SETTING_HEADING_RE.finditer(visual)
1127
+ if match.group(1).strip() not in VISUAL_CATEGORIES
1128
+ }
1129
+ shots = _sections(storyboard, "SHOT")
1130
+ image_pairs = re.findall(r"^## (IMG-[A-Z0-9-]+) · (.+)$", images, re.MULTILINE)
1131
+ image_headings = dict(image_pairs)
1132
+ # A planned picture names the board or keyframe that already decides what it
1133
+ # depicts, so both indexes have to exist before any declaration is read.
1134
+ entries = EntryIndex(images=image_headings, shots=frozenset(shots))
1135
+ all_image_headings = re.findall(r"^## (IMG-[A-Z0-9-]+)\b", images, re.MULTILINE)
1136
+ if len(image_pairs) != len(all_image_headings):
1137
+ errors.append("图片提示词.md: IMG 标题必须包含中文名称")
1138
+ if len(image_pairs) != len(image_headings):
1139
+ errors.append("图片提示词.md: IMG 标题 ID 重复")
1140
+ for image_id, label in image_pairs:
1141
+ if not re.search(r"[\u3400-\u9fff]", label):
1142
+ errors.append(f"{image_id}: IMG 标题缺少中文名称")
1143
+ image_matches = list(re.finditer(r"^## (IMG-[A-Z0-9-]+)\b", images, re.MULTILINE))
1144
+ image_prompts: dict[str, Optional[str]] = {}
1145
+ for index, match in enumerate(image_matches):
1146
+ body = images[
1147
+ match.start() : image_matches[index + 1].start()
1148
+ if index + 1 < len(image_matches)
1149
+ else None
1150
+ ]
1151
+ reference_value = _fields(body, owner=match.group(1), errors=errors).get(
1152
+ "参考", ""
1153
+ )
1154
+ if not reference_value:
1155
+ errors.append(f"{match.group(1)}: 缺少参考字段")
1156
+ elif _is_no_external_reference(reference_value):
1157
+ pass
1158
+ elif "REF-" in reference_value:
1159
+ _references(
1160
+ reference_value, match.group(1), project_root, errors, entries
1161
+ )
1162
+ else:
1163
+ errors.append(
1164
+ f"{match.group(1)}: 参考必须声明无外部参考或使用完整 REF 语法"
1165
+ )
1166
+ image_prompt = _copyable_prompt(body)
1167
+ image_prompts[match.group(1)] = image_prompt
1168
+ if image_prompt is None:
1169
+ errors.append(
1170
+ f"{match.group(1)}: 缺少唯一且非空的可复制提示词"
1171
+ + _copyable_prompt_cause(body)
1172
+ )
1173
+
1174
+ motions = _sections(video, "MOTION")
1175
+ shot_ids = re.findall(r"^## (SHOT-[A-Z0-9-]+)\b", storyboard, re.MULTILINE)
1176
+ motion_ids = re.findall(r"^## (MOTION-[A-Z0-9-]+)\b", video, re.MULTILINE)
1177
+ if len(shot_ids) != len(set(shot_ids)):
1178
+ errors.append("分镜.md: SHOT 标题 ID 重复")
1179
+ if len(motion_ids) != len(set(motion_ids)):
1180
+ errors.append("视频提示词.md: MOTION 标题 ID 重复")
1181
+ named_shots = re.findall(
1182
+ r"^## (SHOT-[A-Z0-9-]+) · ([^\n]+)$", storyboard, re.MULTILINE
1183
+ )
1184
+ named_motions = re.findall(
1185
+ r"^## (MOTION-[A-Z0-9-]+) · ([^\n]+)$", video, re.MULTILINE
1186
+ )
1187
+ if len(named_shots) != len(shot_ids):
1188
+ errors.append("分镜.md: SHOT 标题必须包含中文名称")
1189
+ if len(named_motions) != len(motion_ids):
1190
+ errors.append("视频提示词.md: MOTION 标题必须包含中文名称")
1191
+ for shot_id, label in named_shots:
1192
+ if not re.search(r"[\u3400-\u9fff]", label):
1193
+ errors.append(f"{shot_id}: SHOT 标题缺少中文名称")
1194
+ for motion_id, label in named_motions:
1195
+ if not re.search(r"[\u3400-\u9fff]", label):
1196
+ errors.append(f"{motion_id}: MOTION 标题缺少中文名称")
1197
+ if not shots:
1198
+ errors.append("分镜.md: 没有 SHOT 条目")
1199
+ if not motions:
1200
+ errors.append("视频提示词.md: 没有 MOTION 条目")
1201
+
1202
+ motion_by_shot: dict[str, tuple[str, str, Optional[str]]] = {}
1203
+ motion_duration: dict[str, str] = {}
1204
+ for motion_id, body in motions.items():
1205
+ fields = _fields(body, owner=motion_id, errors=errors)
1206
+ shot_id = _plain(fields.get("分镜", ""))
1207
+ motion_duration[shot_id] = _plain(fields.get("时长", ""))
1208
+ copyable_prompt = _copyable_prompt(body)
1209
+ if not shot_id:
1210
+ errors.append(f"{motion_id}: 缺少分镜字段")
1211
+ elif shot_id in motion_by_shot:
1212
+ errors.append(f"{motion_id}: 分镜 {shot_id} 被多个 MOTION 引用")
1213
+ else:
1214
+ motion_by_shot[shot_id] = (motion_id, body, copyable_prompt)
1215
+ if motion_id.removeprefix("MOTION-") != shot_id.removeprefix("SHOT-"):
1216
+ errors.append(f"{motion_id}: ID 必须与分镜 {shot_id} 一一对应")
1217
+ if copyable_prompt is None:
1218
+ errors.append(
1219
+ f"{motion_id}: 缺少唯一且非空的可复制提示词"
1220
+ + _copyable_prompt_cause(body)
1221
+ )
1222
+
1223
+ if set(motion_by_shot) != set(shots):
1224
+ errors.append("分镜.md/视频提示词.md: SHOT 与 MOTION 未一一对应")
1225
+
1226
+ for shot_id, shot_body in shots.items():
1227
+ fields = _fields(shot_body, owner=shot_id, errors=errors)
1228
+ source_value = fields.get("来源", "")
1229
+ claimed_scenes.update(
1230
+ _shot_sources(source_value, shot_id, scenes, errors)
1231
+ )
1232
+ shot_seconds = _declared_seconds(_plain(fields.get("时长", "")))
1233
+ motion_seconds = _declared_seconds(motion_duration.get(shot_id, ""))
1234
+ if (
1235
+ shot_seconds is not None
1236
+ and motion_seconds is not None
1237
+ and shot_seconds != motion_seconds
1238
+ ):
1239
+ # 时长 is the value actually sent to the execution end and the term
1240
+ # VID-04/VID-13 arithmetic is built on. A downstream copy that drifts
1241
+ # from its accepted upstream is invisible in every other check.
1242
+ errors.append(
1243
+ f"{shot_id}: 分镜时长 {shot_seconds:g} 秒与视频提示词 "
1244
+ f"{motion_seconds:g} 秒不一致;视频提示词只能原样照抄已接受的镜头时长"
1245
+ )
1246
+ image_value = fields.get("图片提示词项", "")
1247
+ if not image_value:
1248
+ errors.append(f"{shot_id}: 缺少图片提示词项字段")
1249
+ image_refs = IMG_RE.findall(image_value)
1250
+ image_remainder = IMG_RE.sub("", image_value).strip(";。 ")
1251
+ if not _is_none(image_value) and (
1252
+ len(image_refs) != len(re.findall(r"\bIMG-[A-Z0-9-]+\b", image_value))
1253
+ or image_remainder
1254
+ ):
1255
+ errors.append(f"{shot_id}: 图片提示词项语法不完整")
1256
+ for image_id, label, _ in image_refs:
1257
+ if image_id not in image_headings:
1258
+ errors.append(f"{shot_id}: IMG 标题不存在: {image_id}")
1259
+ elif label != image_headings[image_id]:
1260
+ errors.append(f"{shot_id}: IMG 中文名称与标题不一致: {image_id}")
1261
+
1262
+ shot_input = fields.get("输入参考图", "")
1263
+ if not shot_input:
1264
+ errors.append(f"{shot_id}: 缺少输入参考图字段")
1265
+ _references(shot_input, shot_id, project_root, errors, entries)
1266
+
1267
+ basis_value = fields.get("视觉依据", "")
1268
+ if not basis_value:
1269
+ errors.append(f"{shot_id}: 缺少视觉依据字段")
1270
+ basis = VisualBasis(False, set(), set())
1271
+ else:
1272
+ basis = _visual_basis(
1273
+ basis_value, shot_id, visual_entries, other_headings, errors
1274
+ )
1275
+ # The keyframe is the only place the frame's contents exist as text, so a
1276
+ # shot without one would make the coverage check below vacuous.
1277
+ keyframe = _copyable_prompt(shot_body, heading=r"冻结关键帧提示词")
1278
+ if keyframe is None:
1279
+ errors.append(
1280
+ f"{shot_id}: 缺少唯一且非空的冻结关键帧提示词"
1281
+ + _copyable_prompt_cause(shot_body, heading=r"冻结关键帧提示词")
1282
+ )
1283
+ elif basis.parsed:
1284
+ _check_named_coverage(
1285
+ keyframe, shot_id, "冻结关键帧提示词", basis, visual_entries, errors
1286
+ )
1287
+ # The closing keyframe is optional — many shots end where they began —
1288
+ # but a heading that is present and malformed is a defect, not a shot
1289
+ # that quietly has no closing frame.
1290
+ if re.search(r"^### 收尾关键帧提示词\s*$", shot_body, re.MULTILINE):
1291
+ closing = _copyable_prompt(shot_body, heading=r"收尾关键帧提示词")
1292
+ if closing is None:
1293
+ errors.append(
1294
+ f"{shot_id}: 收尾关键帧提示词不是唯一且非空的可复制正文"
1295
+ + _copyable_prompt_cause(shot_body, heading=r"收尾关键帧提示词")
1296
+ )
1297
+ elif basis.parsed:
1298
+ _check_named_coverage(
1299
+ closing, shot_id, "收尾关键帧提示词", basis, visual_entries, errors
1300
+ )
1301
+ if basis.parsed and source_value:
1302
+ # A shot's 来源 is its claim on the screenplay: whatever it quotes,
1303
+ # this shot is the one that films it. Quoting an action performed by
1304
+ # someone the frame never shows takes that action off everyone's
1305
+ # list — no other shot claims it, and nothing reports it missing, so
1306
+ # it simply never gets filmed. Same rule as the keyframe: in frame,
1307
+ # or declared 画外.
1308
+ _check_named_coverage(
1309
+ source_value,
1310
+ shot_id,
1311
+ "来源引文",
1312
+ basis,
1313
+ visual_entries,
1314
+ errors,
1315
+ by_entry_name=True,
1316
+ )
1317
+
1318
+ motion = motion_by_shot.get(shot_id)
1319
+ if not motion:
1320
+ continue
1321
+ motion_id, motion_body, copyable_prompt = motion
1322
+ motion_fields = _fields(motion_body, owner=motion_id, errors=errors)
1323
+ if copyable_prompt is not None:
1324
+ _check_spoken_lines(
1325
+ copyable_prompt, motion_id, spoken_sources, errors
1326
+ )
1327
+ motion_input = motion_fields.get("输入参考图", "")
1328
+ _references(motion_input, motion_id, project_root, errors, entries)
1329
+ if _plain(motion_input) != _plain(shot_input):
1330
+ errors.append(f"{motion_id}: 输入参考图与 {shot_id} 不一致")
1331
+
1332
+ if _has_pending_references(shot_input):
1333
+ errors.append(f"{motion_id}: 仍有待补参考图,不能生成最终视频提示词")
1334
+
1335
+ # A `PLAN-...` slot is not a file, but the prompt is still written for
1336
+ # image-conditioned generation: the creator attaches those pictures at
1337
+ # generation time. What decides 生成方式 is whether the shot sends
1338
+ # pictures at all, not whether this suite can read them.
1339
+ sends_pictures = not _is_none(shot_input)
1340
+ expected_mode = "图生视频" if sends_pictures else "文生视频"
1341
+ if _plain(motion_fields.get("生成方式", "")) != expected_mode:
1342
+ errors.append(f"{motion_id}: 生成方式应为{expected_mode}")
1343
+ if not sends_pictures:
1344
+ if not _is_explicit_text_to_video(shot_input):
1345
+ errors.append(
1346
+ f"{motion_id}: 无真实输入参考图时不能静默降级为文生视频;"
1347
+ "请先绑定已有图片、列出待补图片,或记录创作者已明确选择文生视频"
1348
+ )
1349
+ anchor = _plain(motion_fields.get("静态视觉锚点", ""))
1350
+ if not anchor or anchor == "无":
1351
+ errors.append(f"{motion_id}: 文生视频缺少静态视觉锚点")
1352
+ if (
1353
+ copyable_prompt is not None
1354
+ and anchor
1355
+ and anchor != "无"
1356
+ and anchor not in copyable_prompt
1357
+ ):
1358
+ errors.append(f"{motion_id}: 可复制提示词没有包含静态视觉锚点")
1359
+ # In text-to-video the anchor carries the appearance the keyframe
1360
+ # would otherwise have carried, so it is the same claim about the
1361
+ # same frame and answers to the same visual basis. The rest of the
1362
+ # motion body is not checked: it may legitimately name an offscreen
1363
+ # speaker, which SHT-22 excludes.
1364
+ if anchor and anchor != "无" and basis.parsed:
1365
+ _check_named_coverage(
1366
+ anchor, motion_id, "静态视觉锚点", basis, visual_entries, errors
1367
+ )
1368
+
1369
+ for scene_id in scene_headings:
1370
+ if scene_id in claimed_scenes or scene_id in unfilmed:
1371
+ continue
1372
+ errors.append(
1373
+ f"分镜.md: 没有镜头承载场景 {scene_id};"
1374
+ "拍它就写进某一镜的来源,不拍就写进未拍场次并给出理由"
1375
+ )
1376
+ for scene_id in sorted(set(unfilmed) & claimed_scenes):
1377
+ errors.append(f"分镜.md: 场景 {scene_id} 既被镜头承载又记为未拍")
1378
+
1379
+ _check_language_designators(
1380
+ project_root, entries=visual_entries, errors=errors
1381
+ )
1382
+ _check_continuity_locks(
1383
+ locks,
1384
+ shots=shots,
1385
+ motion_by_shot=motion_by_shot,
1386
+ image_prompts=image_prompts,
1387
+ errors=errors,
1388
+ )
1389
+ return errors
1390
+
1391
+
1392
+ def main() -> int:
1393
+ parser = argparse.ArgumentParser(description=__doc__)
1394
+ parser.add_argument("episode", type=Path, help="包含五份 Markdown 的剧集目录")
1395
+ parser.add_argument("--project-root", type=Path, help="用于解析 REF 项目相对路径")
1396
+ args = parser.parse_args()
1397
+ # 诊断与剧集路径都是中文。stdout 重定向时 Windows 用 ANSI 代码页,默认的
1398
+ # strict 处理器会在打印这一步抛错;stderr 早就是 backslashreplace,这里让
1399
+ # stdout 用同一个处理器:能编码就照常显示中文,不能编码才退成转义。
1400
+ reconfigure = getattr(sys.stdout, "reconfigure", None)
1401
+ if reconfigure is not None:
1402
+ reconfigure(errors="backslashreplace")
1403
+ errors = validate_episode(args.episode, args.project_root)
1404
+ if errors:
1405
+ for error in errors:
1406
+ print(f"ERROR: {error}")
1407
+ return 1
1408
+ print(f"OK: {args.episode}")
1409
+ return 0
1410
+
1411
+
1412
+ if __name__ == "__main__":
1413
+ raise SystemExit(main())