@lemoncat7/dsh-partner 2.5.0 → 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 (642) hide show
  1. package/README.md +4 -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 +1 -1
  6. package/docs/releases/2.6.0.md +11 -0
  7. package/docs/releases/2.7.0.md +21 -0
  8. package/lib/agent-runtime.d.ts +3 -1
  9. package/lib/agent-runtime.d.ts.map +1 -1
  10. package/lib/agent-runtime.js +35 -11
  11. package/lib/agent-runtime.js.map +1 -1
  12. package/lib/api/features/attachments-api.d.ts.map +1 -1
  13. package/lib/api/features/attachments-api.js +31 -1
  14. package/lib/api/features/attachments-api.js.map +1 -1
  15. package/lib/api.d.ts +1 -1
  16. package/lib/api.d.ts.map +1 -1
  17. package/lib/api.js +1 -1
  18. package/lib/api.js.map +1 -1
  19. package/lib/attachments/grouped-storage.d.ts +5 -0
  20. package/lib/attachments/grouped-storage.d.ts.map +1 -0
  21. package/lib/attachments/grouped-storage.js +47 -0
  22. package/lib/attachments/grouped-storage.js.map +1 -0
  23. package/lib/attachments/service.d.ts +17 -0
  24. package/lib/attachments/service.d.ts.map +1 -1
  25. package/lib/attachments/service.js +100 -8
  26. package/lib/attachments/service.js.map +1 -1
  27. package/lib/attachments/shared-storage.d.ts +7 -0
  28. package/lib/attachments/shared-storage.d.ts.map +1 -0
  29. package/lib/attachments/shared-storage.js +44 -0
  30. package/lib/attachments/shared-storage.js.map +1 -0
  31. package/lib/attachments/storage-admin.d.ts +6 -0
  32. package/lib/attachments/storage-admin.d.ts.map +1 -0
  33. package/lib/attachments/storage-admin.js +43 -0
  34. package/lib/attachments/storage-admin.js.map +1 -0
  35. package/lib/attachments/storage-types.d.ts +33 -0
  36. package/lib/attachments/storage-types.d.ts.map +1 -0
  37. package/lib/attachments/storage-types.js +2 -0
  38. package/lib/attachments/storage-types.js.map +1 -0
  39. package/lib/attachments/tool.d.ts.map +1 -1
  40. package/lib/attachments/tool.js +16 -5
  41. package/lib/attachments/tool.js.map +1 -1
  42. package/lib/channels/delivery-policy.d.ts.map +1 -1
  43. package/lib/channels/delivery-policy.js +6 -4
  44. package/lib/channels/delivery-policy.js.map +1 -1
  45. package/lib/channels/direct/message-format.js +418 -281
  46. package/lib/channels/direct/message-format.js.map +4 -4
  47. package/lib/channels/manager.d.ts +6 -3
  48. package/lib/channels/manager.d.ts.map +1 -1
  49. package/lib/channels/manager.js +64 -44
  50. package/lib/channels/manager.js.map +1 -1
  51. package/lib/channels/weixin/api.d.ts.map +1 -1
  52. package/lib/channels/weixin/api.js +7 -2
  53. package/lib/channels/weixin/api.js.map +1 -1
  54. package/lib/channels/weixin/errors.d.ts +14 -0
  55. package/lib/channels/weixin/errors.d.ts.map +1 -0
  56. package/lib/channels/weixin/errors.js +29 -0
  57. package/lib/channels/weixin/errors.js.map +1 -0
  58. package/lib/channels/weixin/poll.d.ts +14 -0
  59. package/lib/channels/weixin/poll.d.ts.map +1 -0
  60. package/lib/channels/weixin/poll.js +50 -0
  61. package/lib/channels/weixin/poll.js.map +1 -0
  62. package/lib/client-controller.d.ts.map +1 -1
  63. package/lib/client-controller.js +3 -3
  64. package/lib/client-controller.js.map +1 -1
  65. package/lib/client.d.ts.map +1 -1
  66. package/lib/client.js +996 -645
  67. package/lib/client.js.map +4 -4
  68. package/lib/collaboration/composition.d.ts.map +1 -1
  69. package/lib/collaboration/composition.js +16 -6
  70. package/lib/collaboration/composition.js.map +1 -1
  71. package/lib/collaboration/domain.d.ts +6 -0
  72. package/lib/collaboration/domain.d.ts.map +1 -1
  73. package/lib/collaboration/domain.js.map +1 -1
  74. package/lib/collaboration/service.d.ts.map +1 -1
  75. package/lib/collaboration/service.js +33 -3
  76. package/lib/collaboration/service.js.map +1 -1
  77. package/lib/concern-notification.d.ts.map +1 -1
  78. package/lib/concern-notification.js +2 -1
  79. package/lib/concern-notification.js.map +1 -1
  80. package/lib/concern-tool.js +1 -1
  81. package/lib/concern-tool.js.map +1 -1
  82. package/lib/execution/reply-progress.d.ts.map +1 -1
  83. package/lib/execution/reply-progress.js +4 -2
  84. package/lib/execution/reply-progress.js.map +1 -1
  85. package/lib/execution/service.d.ts +1 -1
  86. package/lib/execution/service.d.ts.map +1 -1
  87. package/lib/execution/service.js +4 -7
  88. package/lib/execution/service.js.map +1 -1
  89. package/lib/index.d.ts.map +1 -1
  90. package/lib/index.js +3 -1
  91. package/lib/index.js.map +1 -1
  92. package/lib/memory-reflection.js +2 -2
  93. package/lib/memory-reflection.js.map +1 -1
  94. package/lib/message-source.d.ts +25 -0
  95. package/lib/message-source.d.ts.map +1 -0
  96. package/lib/message-source.js +8 -0
  97. package/lib/message-source.js.map +1 -0
  98. package/lib/observation-loop.js +4 -4
  99. package/lib/observation-loop.js.map +1 -1
  100. package/lib/pendant/reader.js +1 -1
  101. package/lib/pendant/reader.js.map +1 -1
  102. package/lib/pendant/settings-panel.js +1 -1
  103. package/lib/pendant/settings-panel.js.map +1 -1
  104. package/lib/persona/service.js +1 -1
  105. package/lib/persona/service.js.map +1 -1
  106. package/lib/requirements/attachments.d.ts +7 -0
  107. package/lib/requirements/attachments.d.ts.map +1 -0
  108. package/lib/requirements/attachments.js +26 -0
  109. package/lib/requirements/attachments.js.map +1 -0
  110. package/lib/requirements/domain.d.ts +1 -1
  111. package/lib/requirements/domain.d.ts.map +1 -1
  112. package/lib/requirements/service.d.ts.map +1 -1
  113. package/lib/requirements/service.js +4 -1
  114. package/lib/requirements/service.js.map +1 -1
  115. package/lib/scheduler/channel-origin.d.ts +7 -0
  116. package/lib/scheduler/channel-origin.d.ts.map +1 -0
  117. package/lib/scheduler/channel-origin.js +27 -0
  118. package/lib/scheduler/channel-origin.js.map +1 -0
  119. package/lib/scheduler/continuations.d.ts +2 -2
  120. package/lib/scheduler/continuations.d.ts.map +1 -1
  121. package/lib/scheduler/continuations.js +28 -8
  122. package/lib/scheduler/continuations.js.map +1 -1
  123. package/lib/scheduler/domain.d.ts +13 -0
  124. package/lib/scheduler/domain.d.ts.map +1 -1
  125. package/lib/scheduler/final-reply.d.ts +13 -0
  126. package/lib/scheduler/final-reply.d.ts.map +1 -0
  127. package/lib/scheduler/final-reply.js +21 -0
  128. package/lib/scheduler/final-reply.js.map +1 -0
  129. package/lib/skills/builtin-resources.d.ts +6 -0
  130. package/lib/skills/builtin-resources.d.ts.map +1 -0
  131. package/lib/skills/builtin-resources.js +47 -0
  132. package/lib/skills/builtin-resources.js.map +1 -0
  133. package/lib/skills/builtin.d.ts +2 -5
  134. package/lib/skills/builtin.d.ts.map +1 -1
  135. package/lib/skills/builtin.js +6 -0
  136. package/lib/skills/builtin.js.map +1 -1
  137. package/lib/skills/director-builtins.d.ts +8 -0
  138. package/lib/skills/director-builtins.d.ts.map +1 -0
  139. package/lib/skills/director-builtins.js +65 -0
  140. package/lib/skills/director-builtins.js.map +1 -0
  141. package/lib/skills/drama-builtin.d.ts +3 -0
  142. package/lib/skills/drama-builtin.d.ts.map +1 -0
  143. package/lib/skills/drama-builtin.js +49 -0
  144. package/lib/skills/drama-builtin.js.map +1 -0
  145. package/lib/skills/long-task-continuation.d.ts +2 -2
  146. package/lib/skills/long-task-continuation.d.ts.map +1 -1
  147. package/lib/skills/long-task-continuation.js +2 -2
  148. package/lib/skills/service.d.ts.map +1 -1
  149. package/lib/skills/service.js +8 -3
  150. package/lib/skills/service.js.map +1 -1
  151. package/lib/skills/viora-canvas-builtin.d.ts +3 -0
  152. package/lib/skills/viora-canvas-builtin.d.ts.map +1 -0
  153. package/lib/skills/viora-canvas-builtin.js +19 -0
  154. package/lib/skills/viora-canvas-builtin.js.map +1 -0
  155. package/lib/storage/migrate-layout.d.ts.map +1 -1
  156. package/lib/storage/migrate-layout.js +13 -4
  157. package/lib/storage/migrate-layout.js.map +1 -1
  158. package/lib/tasks/context.js +1 -0
  159. package/lib/tasks/context.js.map +1 -1
  160. package/lib/tasks/continuation.d.ts.map +1 -1
  161. package/lib/tasks/continuation.js +3 -0
  162. package/lib/tasks/continuation.js.map +1 -1
  163. package/lib/tasks/domain.d.ts +2 -0
  164. package/lib/tasks/domain.d.ts.map +1 -1
  165. package/lib/tasks/service.d.ts +2 -1
  166. package/lib/tasks/service.d.ts.map +1 -1
  167. package/lib/tasks/service.js +15 -1
  168. package/lib/tasks/service.js.map +1 -1
  169. package/lib/ui/attachment-storage.d.ts +2 -0
  170. package/lib/ui/attachment-storage.d.ts.map +1 -0
  171. package/lib/ui/attachment-storage.js +59 -0
  172. package/lib/ui/attachment-storage.js.map +1 -0
  173. package/lib/ui/capability-editor.js +1 -1
  174. package/lib/ui/capability-editor.js.map +1 -1
  175. package/lib/ui/companion-access-panel.js +1 -1
  176. package/lib/ui/companion-access-panel.js.map +1 -1
  177. package/lib/ui/companion-create.js +1 -1
  178. package/lib/ui/companion-create.js.map +1 -1
  179. package/lib/ui/companion-picker.js +1 -1
  180. package/lib/ui/companion-picker.js.map +1 -1
  181. package/lib/ui/identity-editor.js +1 -1
  182. package/lib/ui/identity-editor.js.map +1 -1
  183. package/lib/ui/mcp-capabilities.js +1 -1
  184. package/lib/ui/mcp-capabilities.js.map +1 -1
  185. package/lib/ui/mcp-panel.js +1 -1
  186. package/lib/ui/mcp-panel.js.map +1 -1
  187. package/lib/ui/memory/concern-board.js +2 -2
  188. package/lib/ui/memory/concern-board.js.map +1 -1
  189. package/lib/ui/memory/concern-deletion.d.ts +1 -1
  190. package/lib/ui/memory/concern-deletion.d.ts.map +1 -1
  191. package/lib/ui/memory/concern-deletion.js +9 -4
  192. package/lib/ui/memory/concern-deletion.js.map +1 -1
  193. package/lib/ui/memory/concern-panel.js +1 -1
  194. package/lib/ui/memory/concern-panel.js.map +1 -1
  195. package/lib/ui/memory/memory-library.js +1 -1
  196. package/lib/ui/memory/memory-library.js.map +1 -1
  197. package/lib/ui/memory/memory-panel.js +1 -1
  198. package/lib/ui/memory/memory-panel.js.map +1 -1
  199. package/lib/ui/partner-components.js +1 -1
  200. package/lib/ui/partner-components.js.map +1 -1
  201. package/lib/ui/requirement-board.js +1 -1
  202. package/lib/ui/requirement-board.js.map +1 -1
  203. package/lib/ui/requirement-dialog.js +1 -1
  204. package/lib/ui/requirement-dialog.js.map +1 -1
  205. package/lib/ui/requirement-tasks-panel.js +1 -1
  206. package/lib/ui/requirement-tasks-panel.js.map +1 -1
  207. package/lib/ui/schedule-panel.js +1 -1
  208. package/lib/ui/schedule-panel.js.map +1 -1
  209. package/lib/ui/skills-panel.js +1 -1
  210. package/lib/ui/skills-panel.js.map +1 -1
  211. package/lib/ui/storage-settings.d.ts.map +1 -1
  212. package/lib/ui/storage-settings.js +2 -1
  213. package/lib/ui/storage-settings.js.map +1 -1
  214. package/lib/ui/task-board-stage.js +1 -1
  215. package/lib/ui/task-board-stage.js.map +1 -1
  216. package/lib/ui/workspace-components.js +1 -1
  217. package/lib/ui/workspace-components.js.map +1 -1
  218. package/lib/ui/workspace-navigation.d.ts +16 -16
  219. package/lib/ui/workspace-navigation.js +1 -1
  220. package/lib/ui/workspace-navigation.js.map +1 -1
  221. package/package.json +53 -41
  222. package/resources/skills/cinematic-director/manifest.json +205 -0
  223. package/resources/skills/cinematic-director/upstream/CHANGELOG.md +213 -0
  224. package/resources/skills/cinematic-director/upstream/CONTRIBUTING.md +316 -0
  225. package/resources/skills/cinematic-director/upstream/LICENSE +21 -0
  226. package/resources/skills/cinematic-director/upstream/README.md +507 -0
  227. package/resources/skills/cinematic-director/upstream/SKILL.md +420 -0
  228. package/resources/skills/cinematic-director/upstream/assets/beat-sheet-template.md +97 -0
  229. package/resources/skills/cinematic-director/upstream/assets/director-book-template.md +254 -0
  230. package/resources/skills/cinematic-director/upstream/assets/edit-timeline-template.md +147 -0
  231. package/resources/skills/cinematic-director/upstream/assets/keyframe-prompt-template.md +334 -0
  232. package/resources/skills/cinematic-director/upstream/assets/qc-checklist.md +153 -0
  233. package/resources/skills/cinematic-director/upstream/assets/shot-plan-template.md +95 -0
  234. package/resources/skills/cinematic-director/upstream/assets/sound-plan-template.md +181 -0
  235. package/resources/skills/cinematic-director/upstream/assets/video-prompt-template.md +305 -0
  236. package/resources/skills/cinematic-director/upstream/evals/run.md +69 -0
  237. package/resources/skills/cinematic-director/upstream/references/ai-video-tool-adapters.md +527 -0
  238. package/resources/skills/cinematic-director/upstream/references/blocking-and-staging.md +424 -0
  239. package/resources/skills/cinematic-director/upstream/references/cinematic-language.md +595 -0
  240. package/resources/skills/cinematic-director/upstream/references/continuity-bible.md +410 -0
  241. package/resources/skills/cinematic-director/upstream/references/director_styles/01_spielberg.md +192 -0
  242. package/resources/skills/cinematic-director/upstream/references/director_styles/02_hitchcock.md +193 -0
  243. package/resources/skills/cinematic-director/upstream/references/director_styles/03_kubrick.md +196 -0
  244. package/resources/skills/cinematic-director/upstream/references/director_styles/04_kurosawa.md +196 -0
  245. package/resources/skills/cinematic-director/upstream/references/director_styles/05_scorsese.md +195 -0
  246. package/resources/skills/cinematic-director/upstream/references/director_styles/06_fellini.md +197 -0
  247. package/resources/skills/cinematic-director/upstream/references/director_styles/07_bergman.md +197 -0
  248. package/resources/skills/cinematic-director/upstream/references/director_styles/08_tarkovsky.md +184 -0
  249. package/resources/skills/cinematic-director/upstream/references/director_styles/09_wong_kar_wai.md +189 -0
  250. package/resources/skills/cinematic-director/upstream/references/director_styles/10_nolan.md +185 -0
  251. package/resources/skills/cinematic-director/upstream/references/director_styles/11_villeneuve.md +188 -0
  252. package/resources/skills/cinematic-director/upstream/references/director_styles/12_fincher.md +185 -0
  253. package/resources/skills/cinematic-director/upstream/references/director_styles/13_refn.md +186 -0
  254. package/resources/skills/cinematic-director/upstream/references/director_styles/14_bi_gan.md +188 -0
  255. package/resources/skills/cinematic-director/upstream/references/director_styles/15_zhang_yimou.md +192 -0
  256. package/resources/skills/cinematic-director/upstream/references/director_styles/16_hou_hsiao_hsien.md +192 -0
  257. package/resources/skills/cinematic-director/upstream/references/director_styles/17_park_chan_wook.md +192 -0
  258. package/resources/skills/cinematic-director/upstream/references/director_styles/18_malick.md +191 -0
  259. package/resources/skills/cinematic-director/upstream/references/director_styles/19_michael_mann.md +192 -0
  260. package/resources/skills/cinematic-director/upstream/references/director_styles/20_coen_brothers.md +194 -0
  261. package/resources/skills/cinematic-director/upstream/references/director_styles/README.md +89 -0
  262. package/resources/skills/cinematic-director/upstream/references/director_styles/example_comparisons.md +140 -0
  263. package/resources/skills/cinematic-director/upstream/references/editing-and-assembly.md +263 -0
  264. package/resources/skills/cinematic-director/upstream/references/failure-modes.md +594 -0
  265. package/resources/skills/cinematic-director/upstream/references/genre-playbooks.md +407 -0
  266. package/resources/skills/cinematic-director/upstream/references/image-model-adapters.md +290 -0
  267. package/resources/skills/cinematic-director/upstream/references/lighting-and-color.md +349 -0
  268. package/resources/skills/cinematic-director/upstream/references/product-and-macro.md +207 -0
  269. package/resources/skills/cinematic-director/upstream/references/production-workflow.md +238 -0
  270. package/resources/skills/cinematic-director/upstream/references/prompt-lexicon.md +602 -0
  271. package/resources/skills/cinematic-director/upstream/references/sound-and-dialogue.md +364 -0
  272. package/resources/skills/director-skills/manifest.json +541 -0
  273. package/resources/skills/director-skills/upstream/CHANGELOG.md +32 -0
  274. package/resources/skills/director-skills/upstream/LICENSE +21 -0
  275. package/resources/skills/director-skills/upstream/MANIFEST.md +366 -0
  276. package/resources/skills/director-skills/upstream/README.md +251 -0
  277. package/resources/skills/director-skills/upstream/RESEARCH_PROVENANCE.md +37 -0
  278. package/resources/skills/director-skills/upstream/SKILL.md +114 -0
  279. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/SKILL.md +81 -0
  280. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/checklists/continuity_checklist.md +8 -0
  281. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/examples/character_continuity_update.md +9 -0
  282. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/examples/location_continuity_update.md +9 -0
  283. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/continuity_system.md +21 -0
  284. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/golden_image_workflow.md +29 -0
  285. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/identity_anchors.md +14 -0
  286. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/visual_bible_system.md +15 -0
  287. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/character_profile.md +15 -0
  288. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/continuity_checklist.md +11 -0
  289. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/costume_profile.md +12 -0
  290. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/location_profile.md +14 -0
  291. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/prop_profile.md +13 -0
  292. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/visual_style_bible.md +15 -0
  293. package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/tests/continuity_tests.md +6 -0
  294. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/SKILL.md +80 -0
  295. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/checklists/image_prompt_quality_checklist.md +10 -0
  296. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/examples/shot_list_to_image_prompts.md +4 -0
  297. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/image_prompt_components.md +22 -0
  298. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/lighting_and_photorealism.md +19 -0
  299. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/negative_prompt_patterns.md +18 -0
  300. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/prompt_anatomy.md +18 -0
  301. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/character_reference_sheet_prompt.md +5 -0
  302. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/cinematic_still_prompt.md +3 -0
  303. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/keyframe_prompt.md +6 -0
  304. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/location_reference_prompt.md +3 -0
  305. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/prop_prompt.md +3 -0
  306. package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/tests/image_prompt_completeness_tests.md +5 -0
  307. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/SKILL.md +84 -0
  308. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/checklists/video_prompt_quality_checklist.md +9 -0
  309. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/examples/image_prompt_to_video_prompt.md +3 -0
  310. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/ai_film_production_pipeline.md +21 -0
  311. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/audio_motion_timing.md +16 -0
  312. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/camera_lens_movement.md +26 -0
  313. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/negative_prompt_patterns.md +18 -0
  314. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/prompt_anatomy.md +18 -0
  315. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/video_prompt_components.md +25 -0
  316. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/image_to_video_prompt.md +5 -0
  317. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/text_to_video_prompt.md +5 -0
  318. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/video_beat_prompt.md +9 -0
  319. package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/tests/video_prompt_completeness_tests.md +5 -0
  320. package/resources/skills/director-skills/upstream/skills/model-adaptation/SKILL.md +82 -0
  321. package/resources/skills/director-skills/upstream/skills/model-adaptation/checklists/model_export_checklist.md +9 -0
  322. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/failed_model_transfer_diagnosis.md +5 -0
  323. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/image_prompt_translation.md +10 -0
  324. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/same_scene_across_models.md +9 -0
  325. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/universal_prompt_to_model_specific_exports.md +14 -0
  326. package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/video_prompt_translation.md +9 -0
  327. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/failure_modes_by_model.md +13 -0
  328. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/image_model_profiles.md +226 -0
  329. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/model_capability_matrix.md +24 -0
  330. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/model_update_protocol.md +10 -0
  331. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/parameter_compatibility.md +20 -0
  332. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/prompt_translation_rules.md +18 -0
  333. package/resources/skills/director-skills/upstream/skills/model-adaptation/references/video_model_profiles.md +251 -0
  334. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/model_comparison_table.md +4 -0
  335. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/model_specific_prompt_export.md +21 -0
  336. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/multi_model_export_package.md +18 -0
  337. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/prompt_translation_report.md +13 -0
  338. package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/universal_prompt_brief.md +25 -0
  339. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/model_routing_tests.md +8 -0
  340. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/prompt_translation_tests.md +7 -0
  341. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/research_provenance_tests.md +6 -0
  342. package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/unknown_capability_tests.md +7 -0
  343. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/SKILL.md +82 -0
  344. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/checklists/diagnostic_checklist.md +8 -0
  345. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/checklists/release_quality_checklist.md +9 -0
  346. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/examples/failed_output_to_revised_prompt.md +10 -0
  347. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/examples/final_prompt_package_export.md +10 -0
  348. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/failure_modes.md +13 -0
  349. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/iteration_diagnostics.md +17 -0
  350. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/revision_loops.md +17 -0
  351. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/story_voice_release_qc.md +30 -0
  352. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/failed_output_intake.md +13 -0
  353. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/final_production_package.md +11 -0
  354. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/prompt_revision_report.md +17 -0
  355. package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/tests/prompt_quality_checklist_tests.md +6 -0
  356. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/SKILL.md +81 -0
  357. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/checklists/scene_quality_checklist.md +8 -0
  358. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/examples/scene_to_screenplay_excerpt.md +18 -0
  359. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/dialogue_and_subtext.md +15 -0
  360. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/fountain_workflow.md +29 -0
  361. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/scene_construction.md +20 -0
  362. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/screenplay_format.md +18 -0
  363. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/beat_sheet.md +6 -0
  364. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/dialogue_pass.md +9 -0
  365. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/fountain_scene.md +25 -0
  366. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/screenplay_scene.md +19 -0
  367. package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/tests/screenplay_scene_tests.md +6 -0
  368. package/resources/skills/director-skills/upstream/skills/short-film-development/SKILL.md +83 -0
  369. package/resources/skills/director-skills/upstream/skills/short-film-development/checklists/concept_quality_checklist.md +8 -0
  370. package/resources/skills/director-skills/upstream/skills/short-film-development/examples/idea_to_logline.md +13 -0
  371. package/resources/skills/director-skills/upstream/skills/short-film-development/examples/logline_to_outline.md +10 -0
  372. package/resources/skills/director-skills/upstream/skills/short-film-development/references/ai_storytelling_workflow.md +46 -0
  373. package/resources/skills/director-skills/upstream/skills/short-film-development/references/continuity_handoff.md +8 -0
  374. package/resources/skills/director-skills/upstream/skills/short-film-development/references/screenplay_structure.md +19 -0
  375. package/resources/skills/director-skills/upstream/skills/short-film-development/references/story_development_workflow.md +23 -0
  376. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/beat_sheet.md +7 -0
  377. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/idea_intake.md +16 -0
  378. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/logline.md +10 -0
  379. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/short_film_treatment.md +23 -0
  380. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/short_story_plan.md +22 -0
  381. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/story_premise.md +19 -0
  382. package/resources/skills/director-skills/upstream/skills/short-film-development/templates/story_revision_audit.md +22 -0
  383. package/resources/skills/director-skills/upstream/skills/short-film-development/tests/output_format_tests.md +3 -0
  384. package/resources/skills/director-skills/upstream/skills/short-film-development/tests/routing_tests.md +7 -0
  385. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/SKILL.md +77 -0
  386. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/checklists/shotlist_quality_checklist.md +8 -0
  387. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/examples/screenplay_scene_to_shot_list.md +8 -0
  388. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/camera_plan.md +11 -0
  389. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/production_breakdown.md +11 -0
  390. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/shotlist_schema.md +21 -0
  391. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/asset_list.md +7 -0
  392. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/camera_plan.md +10 -0
  393. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/scene_breakdown.md +16 -0
  394. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/shot_list.md +5 -0
  395. package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/tests/screenplay_to_shotlist_tests.md +5 -0
  396. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/SKILL.md +77 -0
  397. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/checklists/style_quality_checklist.md +8 -0
  398. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/examples/genre_to_visual_direction.md +15 -0
  399. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/camera_lens_movement.md +26 -0
  400. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/cinematic_language.md +32 -0
  401. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/genre_style_tone.md +17 -0
  402. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/lighting_and_photorealism.md +19 -0
  403. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/templates/visual_style_bible.md +33 -0
  404. package/resources/skills/director-skills/upstream/skills/style-cinematography-director/tests/style_translation_tests.md +5 -0
  405. package/resources/skills/director-skills/upstream/tests/master_router_tests.md +80 -0
  406. package/resources/skills/drama-production/manifest.json +933 -0
  407. package/resources/skills/drama-production/upstream/LICENSE +21 -0
  408. package/resources/skills/drama-production/upstream/skills/short-drama/SKILL.md +153 -0
  409. package/resources/skills/drama-production/upstream/skills/short-drama/agents/openai.yaml +4 -0
  410. package/resources/skills/drama-production/upstream/skills/short-drama/assets/creator-decision.example.jsonl +4 -0
  411. package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/app.js +1263 -0
  412. package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/index.html +114 -0
  413. package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/styles.css +297 -0
  414. package/resources/skills/drama-production/upstream/skills/short-drama/assets/production-observation.example.jsonl +2 -0
  415. package/resources/skills/drama-production/upstream/skills/short-drama/assets/project-template/short-drama.json +36 -0
  416. package/resources/skills/drama-production/upstream/skills/short-drama/assets/reference-observation.example.jsonl +2 -0
  417. package/resources/skills/drama-production/upstream/skills/short-drama/references/audience-reveal.md +21 -0
  418. package/resources/skills/drama-production/upstream/skills/short-drama/references/contract-and-ownership.md +45 -0
  419. package/resources/skills/drama-production/upstream/skills/short-drama/references/creator-documents.md +230 -0
  420. package/resources/skills/drama-production/upstream/skills/short-drama/references/creator-workflow.md +98 -0
  421. 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
  422. 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
  423. 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
  424. package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//345/256/236/346/213/215.md +75 -0
  425. 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
  426. 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
  427. package/resources/skills/drama-production/upstream/skills/short-drama/references/knowhow-index.md +85 -0
  428. package/resources/skills/drama-production/upstream/skills/short-drama/references/look-development.md +51 -0
  429. package/resources/skills/drama-production/upstream/skills/short-drama/references/pickup-and-alternate.md +24 -0
  430. package/resources/skills/drama-production/upstream/skills/short-drama/references/production-form-profiles.md +126 -0
  431. package/resources/skills/drama-production/upstream/skills/short-drama/references/reference-roles.md +53 -0
  432. package/resources/skills/drama-production/upstream/skills/short-drama/references/runtime-preflight.md +24 -0
  433. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/creator_markdown_check.py +1413 -0
  434. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/dashboard_server.py +1798 -0
  435. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/project_tool.py +2454 -0
  436. package/resources/skills/drama-production/upstream/skills/short-drama/scripts/selftest.py +287 -0
  437. package/resources/skills/drama-production/upstream/skills/short-drama-assets/SKILL.md +76 -0
  438. package/resources/skills/drama-production/upstream/skills/short-drama-assets/agents/openai.yaml +4 -0
  439. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/character-look.example.jsonl +3 -0
  440. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/continuity.example.jsonl +4 -0
  441. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/decisions.example.jsonl +5 -0
  442. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/location-view.example.jsonl +3 -0
  443. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/occurrences.example.jsonl +5 -0
  444. package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/prop-state.example.jsonl +3 -0
  445. package/resources/skills/drama-production/upstream/skills/short-drama-assets/examples/minimal/characters.jsonl +2 -0
  446. package/resources/skills/drama-production/upstream/skills/short-drama-assets/examples/minimal/looks.jsonl +2 -0
  447. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/asset-review-checklist.md +82 -0
  448. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/character-and-look.md +190 -0
  449. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/continuity-delta.md +120 -0
  450. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/continuity-lock.md +122 -0
  451. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/identity-vs-variant.md +140 -0
  452. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/location-and-view.md +84 -0
  453. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/occurrence-extraction.md +108 -0
  454. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/prop-and-state.md +126 -0
  455. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/stage-contract.md +43 -0
  456. package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/voice-direction.md +179 -0
  457. package/resources/skills/drama-production/upstream/skills/short-drama-assets/scripts/asset_check.py +267 -0
  458. package/resources/skills/drama-production/upstream/skills/short-drama-assets/scripts/selftest.py +89 -0
  459. package/resources/skills/drama-production/upstream/skills/short-drama-develop/SKILL.md +157 -0
  460. package/resources/skills/drama-production/upstream/skills/short-drama-develop/agents/openai.yaml +4 -0
  461. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -0
  462. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/creative-brief.md +120 -0
  463. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/episode-map.jsonl +1 -0
  464. package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/story-engine.md +181 -0
  465. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/adaptation-craft.md +219 -0
  466. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/creative-reference-intake.md +57 -0
  467. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/director-brief-craft.md +113 -0
  468. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/episode-design.md +509 -0
  469. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-and-hook-playbook.md +240 -0
  470. 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
  471. 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
  472. 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
  473. 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
  474. 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
  475. 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
  476. 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
  477. 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
  478. 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
  479. 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
  480. 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
  481. 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
  482. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards.md +92 -0
  483. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/mechanism-loop.md +282 -0
  484. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/multi-episode-intake.md +128 -0
  485. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/premise-devices.md +211 -0
  486. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/reveal-reversal-payoff.md +99 -0
  487. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/serial-character-and-memory.md +293 -0
  488. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/stage-contract.md +88 -0
  489. package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/story-craft.md +359 -0
  490. package/resources/skills/drama-production/upstream/skills/short-drama-develop/scripts/episode_intake.py +699 -0
  491. package/resources/skills/drama-production/upstream/skills/short-drama-develop/scripts/selftest.py +47 -0
  492. package/resources/skills/drama-production/upstream/skills/short-drama-edit/SKILL.md +146 -0
  493. package/resources/skills/drama-production/upstream/skills/short-drama-edit/agents/openai.yaml +4 -0
  494. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/README.md +60 -0
  495. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/package.json +15 -0
  496. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  497. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  498. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  499. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  500. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  501. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  502. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  503. package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  504. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/cut-craft.md +38 -0
  505. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/delivery-verify.md +29 -0
  506. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/generated-footage.md +54 -0
  507. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
  508. package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/stage-contract.md +51 -0
  509. package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
  510. package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/selftest.py +520 -0
  511. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/SKILL.md +73 -0
  512. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/agents/openai.yaml +4 -0
  513. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/image-prompt-spec.jsonl.md +187 -0
  514. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/image-prompts.md +42 -0
  515. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/lookdev-frame-spec.jsonl.md +128 -0
  516. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/lookdev-prompts.md +34 -0
  517. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +2 -0
  518. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/character-and-look.md +153 -0
  519. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/common-recipe.md +241 -0
  520. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/edit-and-revision.md +110 -0
  521. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/location-plate.md +151 -0
  522. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/look-and-state-variant.md +97 -0
  523. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/lookdev-frame.md +34 -0
  524. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/production-sheet-recipes.md +177 -0
  525. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/prop-plate.md +137 -0
  526. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/review-and-fixtures.md +92 -0
  527. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/stage-contract.md +32 -0
  528. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/scripts/image_prompt_check.py +443 -0
  529. package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/scripts/selftest.py +95 -0
  530. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/SKILL.md +245 -0
  531. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/agents/openai.yaml +4 -0
  532. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +3 -0
  533. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/adaptation-triage.md +116 -0
  534. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/adaptation-value.md +124 -0
  535. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +155 -0
  536. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/chapter-extraction.md +144 -0
  537. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/stage-contract.md +67 -0
  538. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/scripts/novel_index.py +651 -0
  539. package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/scripts/selftest.py +51 -0
  540. package/resources/skills/drama-production/upstream/skills/short-drama-produce/SKILL.md +183 -0
  541. package/resources/skills/drama-production/upstream/skills/short-drama-produce/agents/openai.yaml +4 -0
  542. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/adapter-contract.md +219 -0
  543. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/gpt-image-2.md +24 -0
  544. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-h3-video.md +86 -0
  545. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-music.md +25 -0
  546. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  547. package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/seedance.md +64 -0
  548. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/fixture_adapter.py +77 -0
  549. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/production_tool.py +2205 -0
  550. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/provider_adapters.py +1747 -0
  551. package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/selftest.py +135 -0
  552. package/resources/skills/drama-production/upstream/skills/short-drama-review/SKILL.md +118 -0
  553. package/resources/skills/drama-production/upstream/skills/short-drama-review/agents/openai.yaml +4 -0
  554. package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/finding-template.jsonl +2 -0
  555. package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/supersession-decision.example.json +42 -0
  556. package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/verdict-template.json +32 -0
  557. package/resources/skills/drama-production/upstream/skills/short-drama-review/examples/minimal-findings.jsonl +2 -0
  558. package/resources/skills/drama-production/upstream/skills/short-drama-review/examples/minimal-verdict.json +34 -0
  559. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/anti-template-repair.md +109 -0
  560. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/production-quality-gates.md +276 -0
  561. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/project-calibration.md +83 -0
  562. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/review-method.md +81 -0
  563. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-assets-prompts.md +111 -0
  564. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-source-analysis.md +108 -0
  565. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-story-script.md +264 -0
  566. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-visual-motion.md +184 -0
  567. package/resources/skills/drama-production/upstream/skills/short-drama-review/references/stage-contract.md +28 -0
  568. package/resources/skills/drama-production/upstream/skills/short-drama-review/scripts/review_check.py +325 -0
  569. package/resources/skills/drama-production/upstream/skills/short-drama-review/scripts/selftest.py +87 -0
  570. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/SKILL.md +175 -0
  571. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/agents/openai.yaml +4 -0
  572. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +2 -0
  573. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/coverage-template.json +49 -0
  574. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/keyframe-prompts.md +36 -0
  575. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/keyframe-template.jsonl +2 -0
  576. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +12 -0
  577. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +2 -0
  578. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/shot-template.jsonl +2 -0
  579. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/blocking-playbooks.md +216 -0
  580. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
  581. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/coverage-audition.md +34 -0
  582. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/keyframe-craft.md +174 -0
  583. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  584. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/production-shot-grammar.md +279 -0
  585. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/review-and-fixtures.md +36 -0
  586. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/scene-visual-plan.md +30 -0
  587. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +155 -0
  588. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/shot-craft.md +266 -0
  589. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/shot-revision-identity.md +25 -0
  590. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/stage-contract.md +57 -0
  591. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/scripts/selftest.py +191 -0
  592. package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/scripts/storyboard_check.py +824 -0
  593. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/SKILL.md +154 -0
  594. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/agents/openai.yaml +4 -0
  595. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +21 -0
  596. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/delivery-container.jsonl.md +101 -0
  597. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +217 -0
  598. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +2 -0
  599. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/music-spec.jsonl.md +31 -0
  600. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/performance.fragment.json +38 -0
  601. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/video-prompts.md +67 -0
  602. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +2 -0
  603. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/camera-audio-continuity.md +259 -0
  604. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/delivery-profile.md +273 -0
  605. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/generability.md +93 -0
  606. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/minimax-h3.md +253 -0
  607. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/motion-recipe.md +315 -0
  608. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/performance-action-timing.md +209 -0
  609. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/production-prompt-grammar.md +304 -0
  610. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/review-and-fixtures.md +139 -0
  611. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/seedance-2.0.md +50 -0
  612. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/seedance-2.5.md +122 -0
  613. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/stage-contract.md +60 -0
  614. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/target-model-profile.md +116 -0
  615. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/container_check.py +329 -0
  616. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/motion_timing_check.py +450 -0
  617. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/music_spec_check.py +327 -0
  618. package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/selftest.py +82 -0
  619. package/resources/skills/drama-production/upstream/skills/short-drama-write/SKILL.md +99 -0
  620. package/resources/skills/drama-production/upstream/skills/short-drama-write/agents/openai.yaml +4 -0
  621. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/beats.jsonl +2 -0
  622. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/episode-card-standalone.json +120 -0
  623. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/episode-card.json +55 -0
  624. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/screenplay.md +36 -0
  625. package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/voice-record-sheet.jsonl.md +89 -0
  626. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/dialogue-craft.md +526 -0
  627. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/production-format-dialect.md +273 -0
  628. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/scene-handoff-capsule.md +25 -0
  629. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/scene-sound-dramaturgy.md +43 -0
  630. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/screenplay-format.md +266 -0
  631. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/script-craft.md +745 -0
  632. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/stage-contract.md +36 -0
  633. package/resources/skills/drama-production/upstream/skills/short-drama-write/references/substitutable-realization.md +107 -0
  634. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/duration_estimate.py +309 -0
  635. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/screenplay_index.py +1091 -0
  636. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/selftest.py +229 -0
  637. package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/voice_sheet_check.py +416 -0
  638. package/resources/skills/viora-canvas-production/manifest.json +23 -0
  639. package/resources/skills/viora-canvas-production/references/canvas-contract.md +50 -0
  640. package/resources/skills/viora-canvas-production/references/creative-production.md +50 -0
  641. package/resources/skills/viora-canvas-production/references/generation-delivery.md +36 -0
  642. package/resources/skills/viora-canvas-production/references/production-ledger.md +25 -0
@@ -0,0 +1,1156 @@
1
+ #!/usr/bin/env python3
2
+ """Assemble a short-drama episode from its cut list.
3
+
4
+ Three subcommands, deliberately separated so a report can never borrow one's
5
+ evidence for another's claim:
6
+
7
+ ``check`` parse and cross-check ``剪辑单.md`` against the project. No rendering.
8
+ ``render`` cut, join, burn subtitles and normalize loudness into 制作成果/成片/.
9
+ ``verify`` measure an already-rendered film and print the numbers.
10
+
11
+ ``verify`` prints measurements, never verdicts. Whether the film is any good is
12
+ a question for review or for the creator, and no number here answers it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import re
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ import tempfile
24
+ from pathlib import Path
25
+ from typing import Any, NamedTuple, Optional, Sequence
26
+
27
+ MINIMUM_PYTHON = (3, 9)
28
+ if sys.version_info < MINIMUM_PYTHON:
29
+ raise SystemExit(
30
+ "short-drama-edit needs Python {}.{} or newer; this interpreter is {}.{}".format(
31
+ *MINIMUM_PYTHON, sys.version_info.major, sys.version_info.minor
32
+ )
33
+ )
34
+
35
+ CUT_LIST_NAME = "剪辑单.md"
36
+ MOTION_DOCUMENT = "视频提示词.md"
37
+ SCREENPLAY_DOCUMENT = "剧本.md"
38
+ OUTPUT_DIRECTORY = Path("制作成果") / "成片"
39
+ SEGMENT_DIRECTORY = "分段"
40
+ # A CJK subtitle needs a font that actually has the glyphs. libass falls back
41
+ # through fontconfig when this one is absent, which is the right behaviour: a
42
+ # missing font shows as a different typeface, never as empty boxes.
43
+ SUBTITLE_FONT = "Noto Sans CJK SC"
44
+ # The optional Remotion route ships as source only. Its `node_modules` is
45
+ # hundreds of megabytes of third-party code that is neither creative content nor
46
+ # part of the skill, so the runnable copy lives outside the project entirely --
47
+ # the same rule the suite applies to adapter configuration and credentials.
48
+ REMOTION_SOURCE = Path(__file__).resolve().parent.parent / "assets" / "remotion"
49
+ REMOTION_SOURCE_FILES = (
50
+ "package.json",
51
+ "tsconfig.json",
52
+ "remotion.config.ts",
53
+ "src/index.ts",
54
+ "src/schema.ts",
55
+ "src/Root.tsx",
56
+ "src/Subtitles.tsx",
57
+ "src/font.ts",
58
+ )
59
+ DEFAULT_REMOTION_WORKSPACE = Path.home() / ".cache" / "short-drama-edit" / "remotion"
60
+ REMOTION_COMPOSITION = "Subtitles"
61
+ # Remotion defaults its worker count to the machine's core count, and each worker
62
+ # is a browser holding a full frame. On a 10-core / 8 GB laptop that took the
63
+ # machine down mid-render. Raise this only after measuring the host.
64
+ DEFAULT_REMOTION_CONCURRENCY = 2
65
+ SUBTITLE_RENDERERS = ("ffmpeg", "remotion")
66
+
67
+ CUT_HEADING = re.compile(r"^##\s+(CUT-[^\s·]+)\s*(?:·\s*(.*))?$")
68
+ MOTION_HEADING = re.compile(r"^##\s+(MOTION-[^\s·]+)")
69
+ FIELD = re.compile(r"^-\s*([^:]+):\s*(.*)$")
70
+ UNUSED_LINE = re.compile(r"^-\s*未采用镜头:\s*(.*)$")
71
+ # A source line is "MOTION-... · relative/path". The separator is the same
72
+ # middle dot the storyboard uses for reference slots, so the two documents read
73
+ # alike; a plain slash would collide with the path itself.
74
+ SOURCE = re.compile(r"^(MOTION-\S+)\s*·\s*(.+?)\s*$")
75
+ SUBTITLE_WINDOW = re.compile(r"^\s*([0-9.]+)\s*[-–~]\s*([0-9.]+)\s*$")
76
+ # One shot can carry several lines: an exchange of three is one shot, not three.
77
+ # Numbered fields keep them ordered and let each one state its own window.
78
+ SUBTITLE_FIELD = re.compile(r"^字幕(?:\s*(\d+))?$")
79
+ SUBTITLE_CUE = re.compile(r"^\s*([0-9.]+)\s*[-–~]\s*([0-9.]+)\s+(.+?)\s*$")
80
+ # A shot-match is three numbers and nothing else. Anything richer belongs in a
81
+ # grading tool, and anything implicit belongs nowhere: a correction the cut list
82
+ # does not state is a correction no reviewer can see.
83
+ PICTURE_KEYS = {"亮度": "brightness", "饱和": "saturation", "色温": "warmth"}
84
+ PICTURE_TERM = re.compile(r"(亮度|饱和|色温)\s*([+-]?[0-9]*\.?[0-9]+)")
85
+ # Each term's real range, not a symmetric magnitude: ffmpeg's `eq` takes
86
+ # saturation in 0..3, so a negative value that passes an abs() bound is accepted
87
+ # by the document and then rejected by the renderer, halfway through a render.
88
+ PICTURE_LIMITS = {
89
+ "brightness": (-0.25, 0.25),
90
+ "saturation": (0.0, 2.0),
91
+ "warmth": (-30.0, 30.0),
92
+ }
93
+ TOLERANCE = 0.005
94
+ # ffmpeg's `noise` strength runs to 100, which is snow, not grain. The useful
95
+ # band for a finished film is single digits; the cap keeps a typo from shipping
96
+ # a broken-signal look that measures perfectly fine.
97
+ GRAIN_LIMIT = 20.0
98
+
99
+
100
+ class EditError(Exception):
101
+ """A defect in the cut list or its inputs, reported rather than raised through."""
102
+
103
+
104
+ class Cut(NamedTuple):
105
+ cut_id: str
106
+ title: str
107
+ motion: str
108
+ media: str
109
+ start: float
110
+ end: float
111
+ declared: float
112
+ subtitles: tuple[tuple[Optional[float], Optional[float], str], ...]
113
+ picture: dict[str, float]
114
+ line_number: int
115
+
116
+
117
+ class Delivery(NamedTuple):
118
+ target_seconds: Optional[float]
119
+ loudness_lufs: Optional[float]
120
+ burn_subtitles: bool
121
+ frame_size: Optional[tuple[int, int]]
122
+ fps: Optional[float]
123
+ grain: Optional[float]
124
+
125
+
126
+ def _seconds(raw: str, *, field: str, line: int) -> float:
127
+ try:
128
+ return float(raw.strip())
129
+ except ValueError as error:
130
+ raise EditError(f"{CUT_LIST_NAME}:{line}: {field} 不是秒数: {raw!r}") from error
131
+
132
+
133
+ def _parse_delivery(lines: Sequence[str]) -> Delivery:
134
+ target = None
135
+ loudness = None
136
+ burn = True
137
+ frame_size: Optional[tuple[int, int]] = None
138
+ fps: Optional[float] = None
139
+ grain: Optional[float] = None
140
+ for raw in lines:
141
+ match = FIELD.match(raw)
142
+ if not match:
143
+ continue
144
+ name, value = match.group(1).strip(), match.group(2).strip()
145
+ if name == "成片目标时长":
146
+ found = re.search(r"[0-9]+(?:\.[0-9]+)?", value)
147
+ if found:
148
+ target = float(found.group(0))
149
+ elif name == "交付响度":
150
+ found = re.search(r"-?[0-9]+(?:\.[0-9]+)?", value)
151
+ if found:
152
+ loudness = float(found.group(0))
153
+ elif name == "字幕":
154
+ burn = "无" != value.strip() and "不烧" not in value
155
+ elif name == "颗粒":
156
+ # Grain belongs to the delivery spec rather than to a cut, for the
157
+ # same reason loudness does: applied per cut it would become one
158
+ # more thing that differs between segments, which is the defect it
159
+ # is here to cover.
160
+ if value.strip() in {"无", "不加"}:
161
+ grain = None
162
+ else:
163
+ found = re.search(r"[0-9]+(?:\.[0-9]+)?", value)
164
+ if found:
165
+ amount = float(found.group(0))
166
+ if not 0.0 <= amount <= GRAIN_LIMIT:
167
+ raise EditError(
168
+ f"{CUT_LIST_NAME}: 颗粒 {amount} 超出 0–{GRAIN_LIMIT:g};"
169
+ "这一档以上不再像胶片,像信号故障"
170
+ )
171
+ grain = amount or None
172
+ elif name == "画幅与帧率":
173
+ size = re.search(r"([0-9]{2,5})\s*[×x*]\s*([0-9]{2,5})", value)
174
+ if size:
175
+ frame_size = (int(size.group(1)), int(size.group(2)))
176
+ rate = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*fps", value, re.I)
177
+ if rate:
178
+ fps = float(rate.group(1))
179
+ return Delivery(target, loudness, burn, frame_size, fps, grain)
180
+
181
+
182
+ def parse_cut_list(path: Path) -> tuple[Delivery, list[Cut], list[str]]:
183
+ """Read 剪辑单.md into a delivery spec, ordered cuts, and unused-material notes."""
184
+
185
+ if not path.is_file():
186
+ raise EditError(f"没有 {path}")
187
+ lines = path.read_text(encoding="utf-8").splitlines()
188
+ preamble: list[str] = []
189
+ unused: list[str] = []
190
+ cuts: list[Cut] = []
191
+ current: Optional[dict[str, Any]] = None
192
+
193
+ def close(pending: Optional[dict[str, Any]]) -> None:
194
+ if pending is None:
195
+ return
196
+ cuts.append(_finish_cut(pending))
197
+
198
+ for number, raw in enumerate(lines, start=1):
199
+ heading = CUT_HEADING.match(raw)
200
+ if heading:
201
+ close(current)
202
+ current = {
203
+ "cut_id": heading.group(1),
204
+ "title": (heading.group(2) or "").strip(),
205
+ "line": number,
206
+ "fields": {},
207
+ }
208
+ continue
209
+ if raw.startswith("## "):
210
+ close(current)
211
+ current = None
212
+ continue
213
+ if current is None:
214
+ unused_match = UNUSED_LINE.match(raw)
215
+ if unused_match:
216
+ unused.extend(
217
+ item.strip() for item in unused_match.group(1).split(";") if item.strip()
218
+ )
219
+ preamble.append(raw)
220
+ continue
221
+ field = FIELD.match(raw)
222
+ if field:
223
+ current["fields"][field.group(1).strip()] = (field.group(2).strip(), number)
224
+ close(current)
225
+ return _parse_delivery(preamble), cuts, unused
226
+
227
+
228
+ def _finish_cut(pending: dict[str, Any]) -> Cut:
229
+ fields: dict[str, tuple[str, int]] = pending["fields"]
230
+ cut_id: str = pending["cut_id"]
231
+ line: int = pending["line"]
232
+
233
+ def required(name: str) -> tuple[str, int]:
234
+ if name not in fields:
235
+ raise EditError(f"{CUT_LIST_NAME}:{line}: {cut_id} 缺少「{name}」")
236
+ return fields[name]
237
+
238
+ source_raw, source_line = required("来源")
239
+ source = SOURCE.match(source_raw)
240
+ if not source:
241
+ raise EditError(
242
+ f"{CUT_LIST_NAME}:{source_line}: {cut_id} 的来源要写成 "
243
+ f"「MOTION-... · 项目相对路径」,当前是 {source_raw!r}"
244
+ )
245
+ start_raw, start_line = required("入点")
246
+ end_raw, end_line = required("出点")
247
+ declared_raw, declared_line = required("时长")
248
+ subtitles = _parse_subtitles(fields, cut_id=cut_id, line=line)
249
+ picture: dict[str, float] = {}
250
+ picture_raw = fields.get("画面")
251
+ if picture_raw is not None:
252
+ text, where = picture_raw
253
+ if text.strip() not in {"", "无", "不校"}:
254
+ for label, value in PICTURE_TERM.findall(text):
255
+ key = PICTURE_KEYS[label]
256
+ number = float(value)
257
+ low, high = PICTURE_LIMITS[key]
258
+ if not low <= number <= high:
259
+ raise EditError(
260
+ f"{CUT_LIST_NAME}:{where}: {cut_id} 的画面「{label}」超出允许范围"
261
+ f"({low:g} 到 {high:g}):{number}"
262
+ )
263
+ picture[key] = number
264
+ if not picture:
265
+ raise EditError(
266
+ f"{CUT_LIST_NAME}:{where}: {cut_id} 的画面写了内容但没有可执行的项;"
267
+ "只认「亮度 <数>」「饱和 <数>」「色温 <数>」,不需要校正时写「无」"
268
+ )
269
+ return Cut(
270
+ cut_id=cut_id,
271
+ title=pending["title"],
272
+ motion=source.group(1),
273
+ media=source.group(2),
274
+ start=_seconds(start_raw, field="入点", line=start_line),
275
+ end=_seconds(end_raw, field="出点", line=end_line),
276
+ declared=_seconds(declared_raw, field="时长", line=declared_line),
277
+ subtitles=subtitles,
278
+ picture=picture,
279
+ line_number=line,
280
+ )
281
+
282
+
283
+ def _parse_subtitles(
284
+ fields: dict[str, tuple[str, int]], *, cut_id: str, line: int
285
+ ) -> tuple[tuple[Optional[float], Optional[float], str], ...]:
286
+ """Read every subtitle a cut declares, in written order.
287
+
288
+ A shot is not one line. The exchange "就是什么 / 就是少了点东西 / 少了什么" is a
289
+ single over-shoulder shot carrying three, and a cut list that can hold only
290
+ one of them silently drops the other two — the film then plays lines that
291
+ never reach the screen, and nothing reports it.
292
+ """
293
+
294
+ numbered: list[tuple[int, str, int]] = []
295
+ plain: Optional[tuple[str, int]] = None
296
+ for key, (value, where) in fields.items():
297
+ match = SUBTITLE_FIELD.match(key.strip())
298
+ if not match:
299
+ continue
300
+ if match.group(1) is None:
301
+ plain = (value, where)
302
+ else:
303
+ numbered.append((int(match.group(1)), value, where))
304
+
305
+ if numbered and plain is not None:
306
+ raise EditError(
307
+ f"{CUT_LIST_NAME}:{line}: {cut_id} 同时写了「字幕」和「字幕 N」;"
308
+ "一段用一种写法——一句用「字幕」,多句全部编号"
309
+ )
310
+
311
+ if numbered:
312
+ numbered.sort()
313
+ expected = list(range(1, len(numbered) + 1))
314
+ if [item[0] for item in numbered] != expected:
315
+ raise EditError(
316
+ f"{CUT_LIST_NAME}:{line}: {cut_id} 的字幕编号要从 1 连续排到 "
317
+ f"{len(numbered)},当前是 {[item[0] for item in numbered]}"
318
+ )
319
+ cues: list[tuple[Optional[float], Optional[float], str]] = []
320
+ for index, value, where in numbered:
321
+ found = SUBTITLE_CUE.match(value)
322
+ if not found:
323
+ raise EditError(
324
+ f"{CUT_LIST_NAME}:{where}: {cut_id} 的「字幕 {index}」要写成"
325
+ "「<起>-<止> <台词>」,多句必须各自带时间"
326
+ )
327
+ cues.append((float(found.group(1)), float(found.group(2)), found.group(3)))
328
+ for earlier, later in zip(cues, cues[1:]):
329
+ if later[0] is not None and earlier[1] is not None and later[0] < earlier[1]:
330
+ raise EditError(
331
+ f"{CUT_LIST_NAME}:{line}: {cut_id} 的字幕时间重叠:两句不能同时在屏上"
332
+ )
333
+ return tuple(cues)
334
+
335
+ if plain is None or plain[0].strip() == "无":
336
+ return ()
337
+ window_raw = fields.get("字幕时间")
338
+ if window_raw is None:
339
+ return ((None, None, plain[0].strip()),)
340
+ found = SUBTITLE_WINDOW.match(window_raw[0])
341
+ if not found:
342
+ raise EditError(
343
+ f"{CUT_LIST_NAME}:{window_raw[1]}: {cut_id} 的字幕时间要写成「起-止」秒数"
344
+ )
345
+ return ((float(found.group(1)), float(found.group(2)), plain[0].strip()),)
346
+
347
+
348
+ def _which(name: str) -> Optional[str]:
349
+ return shutil.which(name)
350
+
351
+
352
+ def _require(name: str) -> str:
353
+ found = _which(name)
354
+ if found is None:
355
+ raise EditError(
356
+ f"PATH 上没有 {name}。本阶段要渲染和测量真实媒体,没有它就没有可报告的事实;"
357
+ "先安装 ffmpeg,不用别的手段近似。"
358
+ )
359
+ return found
360
+
361
+
362
+ def probe_duration(media: Path) -> float:
363
+ probe = _require("ffprobe")
364
+ result = subprocess.run(
365
+ [probe, "-v", "error", "-show_entries", "format=duration",
366
+ "-of", "default=nw=1:nk=1", str(media)],
367
+ capture_output=True, text=True, check=False,
368
+ )
369
+ if result.returncode != 0:
370
+ raise EditError(f"ffprobe 读不出时长: {media} ({result.stderr.strip()})")
371
+ return float(result.stdout.strip())
372
+
373
+
374
+ def probe_stream(media: Path) -> dict[str, Any]:
375
+ probe = _require("ffprobe")
376
+ result = subprocess.run(
377
+ [probe, "-v", "error", "-select_streams", "v:0", "-show_entries",
378
+ "stream=width,height,r_frame_rate", "-show_entries", "format=duration",
379
+ "-of", "json", str(media)],
380
+ capture_output=True, text=True, check=False,
381
+ )
382
+ if result.returncode != 0:
383
+ raise EditError(f"ffprobe 读不出流信息: {media} ({result.stderr.strip()})")
384
+ document = json.loads(result.stdout)
385
+ stream = (document.get("streams") or [{}])[0]
386
+ rate = stream.get("r_frame_rate", "0/1")
387
+ numerator, _, denominator = rate.partition("/")
388
+ fps = float(numerator) / float(denominator) if float(denominator or 0) else 0.0
389
+ return {
390
+ "width": stream.get("width"),
391
+ "height": stream.get("height"),
392
+ "fps": round(fps, 3),
393
+ "duration": float(document.get("format", {}).get("duration", 0.0)),
394
+ }
395
+
396
+
397
+ def _unaccounted_shots(known: set[str], cuts: Sequence[Cut], unused: Sequence[str]) -> list[str]:
398
+ """Require each source to be used or explicitly omitted with a reason."""
399
+
400
+ used = {cut.motion for cut in cuts}
401
+ excused = set()
402
+ for note in unused:
403
+ match = re.fullmatch(r"(MOTION-[\w-]+)\s*[((]理由[::]\s*(.+?)[))]", note.strip())
404
+ if match and match.group(2).strip():
405
+ excused.add(match.group(1))
406
+ missing = sorted(known - used - excused)
407
+ if not missing:
408
+ return []
409
+ return [
410
+ f"{CUT_LIST_NAME}: 以下镜头未采用,且缺少「未采用镜头」及理由:"
411
+ + "、".join(missing)
412
+ ]
413
+
414
+
415
+ def check_cuts(
416
+ episode: Path,
417
+ cuts: Sequence[Cut],
418
+ project_root: Path,
419
+ *,
420
+ probe: bool,
421
+ unused: Sequence[str] = (),
422
+ ) -> list[str]:
423
+ """Every mechanical cross-check the cut list can be held to. Returns findings."""
424
+
425
+ findings: list[str] = []
426
+ if not cuts:
427
+ findings.append(f"{CUT_LIST_NAME}: 没有 CUT 条目")
428
+ return findings
429
+
430
+ seen: set[str] = set()
431
+ for cut in cuts:
432
+ if cut.cut_id in seen:
433
+ findings.append(f"{CUT_LIST_NAME}:{cut.line_number}: CUT ID 重复: {cut.cut_id}")
434
+ seen.add(cut.cut_id)
435
+
436
+ motion_path = episode / MOTION_DOCUMENT
437
+ if motion_path.is_file():
438
+ known = {
439
+ match.group(1)
440
+ for match in (
441
+ MOTION_HEADING.match(line)
442
+ for line in motion_path.read_text(encoding="utf-8").splitlines()
443
+ )
444
+ if match
445
+ }
446
+ for cut in cuts:
447
+ if cut.motion not in known:
448
+ findings.append(
449
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的来源 "
450
+ f"{cut.motion} 不在《{MOTION_DOCUMENT}》中"
451
+ )
452
+ findings.extend(_unaccounted_shots(known, cuts, unused))
453
+ else:
454
+ findings.append(f"没有 {motion_path},来源 MOTION 无法核对")
455
+
456
+ screenplay = ""
457
+ screenplay_path = episode / SCREENPLAY_DOCUMENT
458
+ if screenplay_path.is_file():
459
+ screenplay = screenplay_path.read_text(encoding="utf-8")
460
+
461
+ media_format: Optional[tuple[Any, Any, float]] = None
462
+ for cut in cuts:
463
+ span = cut.end - cut.start
464
+ if cut.start < 0:
465
+ findings.append(f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 入点为负")
466
+ if span <= 0:
467
+ findings.append(
468
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点不晚于入点"
469
+ )
470
+ elif abs(span - cut.declared) > TOLERANCE:
471
+ findings.append(
472
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点 - 入点 = "
473
+ f"{span:.2f},与「时长:{cut.declared:.2f}」不符"
474
+ )
475
+ media = _resolve_media(episode, project_root, cut.media)
476
+ if media is None:
477
+ findings.append(
478
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的素材不存在: {cut.media}"
479
+ )
480
+ elif probe:
481
+ stream = probe_stream(media)
482
+ available = stream["duration"]
483
+ current_format = (stream["width"], stream["height"], stream["fps"])
484
+ if media_format is None:
485
+ media_format = current_format
486
+ elif current_format != media_format:
487
+ findings.append(
488
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的画幅或帧率 "
489
+ f"{current_format} 与首段 {media_format} 不一致;"
490
+ "先在外部统一素材规格,再更新来源路径与入出点"
491
+ )
492
+ if cut.end > available + TOLERANCE:
493
+ findings.append(
494
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点 {cut.end:.2f} "
495
+ f"超过素材实际时长 {available:.2f}"
496
+ )
497
+ for window_start, window_end, text in cut.subtitles:
498
+ if screenplay and _normalize(text) not in _normalize(screenplay):
499
+ findings.append(
500
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的字幕在《"
501
+ f"{SCREENPLAY_DOCUMENT}》里找不到原文: {text}"
502
+ )
503
+ if window_start is None or window_end is None:
504
+ continue
505
+ if not 0 <= window_start < window_end <= span + TOLERANCE:
506
+ findings.append(
507
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的字幕时间超出本段区间"
508
+ f": {window_start}-{window_end}"
509
+ )
510
+
511
+ findings.extend(_overlap_findings(cuts))
512
+ findings.extend(_stale_window_findings(episode, project_root, cuts))
513
+ return findings
514
+
515
+
516
+ def _stale_window_findings(
517
+ episode: Path, project_root: Path, cuts: Sequence[Cut]
518
+ ) -> list[str]:
519
+ """Material newer than the cut list means the timings were measured on something else.
520
+
521
+ Regenerating one shot is routine — the take was hazy, the action was wrong.
522
+ What is easy to forget is that every subtitle window bound to it was reverse-
523
+ engineered from the *old* take's audio. Nothing else notices: the numbers are
524
+ still in range, the render still succeeds, and the subtitle simply appears at
525
+ a moment when nobody is speaking. This is the one form of staleness the file
526
+ system can see, so it is reported rather than trusted to memory.
527
+ """
528
+
529
+ listing = episode / CUT_LIST_NAME
530
+ try:
531
+ authored = listing.stat().st_mtime
532
+ except OSError:
533
+ return []
534
+ findings: list[str] = []
535
+ for cut in cuts:
536
+ if not any(start is not None for start, _, _ in cut.subtitles):
537
+ continue
538
+ media = _resolve_media(episode, project_root, cut.media)
539
+ if media is None:
540
+ continue
541
+ try:
542
+ changed = media.stat().st_mtime
543
+ except OSError:
544
+ continue
545
+ if changed > authored + 1.0:
546
+ findings.append(
547
+ f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的素材比剪辑单新"
548
+ f"({cut.media});这一段的字幕时间是按旧素材的发声区间反推的,"
549
+ "重出之后必须重测再改,不能沿用"
550
+ )
551
+ return findings
552
+
553
+
554
+ def _overlap_findings(cuts: Sequence[Cut]) -> list[str]:
555
+ """Two cuts drawn from one file must not reuse the same frames (EDT-06)."""
556
+
557
+ findings: list[str] = []
558
+ by_media: dict[str, list[Cut]] = {}
559
+ for cut in cuts:
560
+ by_media.setdefault(cut.media, []).append(cut)
561
+ for media, group in by_media.items():
562
+ ordered = sorted(group, key=lambda item: item.start)
563
+ for earlier, later in zip(ordered, ordered[1:]):
564
+ if later.start < earlier.end - TOLERANCE:
565
+ findings.append(
566
+ f"{CUT_LIST_NAME}: {earlier.cut_id} 与 {later.cut_id} 在同一素材 "
567
+ f"{media} 上区间重叠({later.start:.2f} < {earlier.end:.2f})"
568
+ )
569
+ return findings
570
+
571
+
572
+ def _normalize(text: str) -> str:
573
+ """Compare dialogue ignoring punctuation and whitespace, never ignoring characters."""
574
+
575
+ return re.sub(r"[\s,。!?、;:…—·\-“”‘’\"'()()]", "", text)
576
+
577
+
578
+ def _resolve_media(episode: Path, project_root: Path, relative: str) -> Optional[Path]:
579
+ for base in (episode, project_root):
580
+ candidate = (base / relative).resolve()
581
+ if candidate.is_file():
582
+ return candidate
583
+ return None
584
+
585
+
586
+ def render(
587
+ episode: Path,
588
+ project_root: Path,
589
+ cuts: Sequence[Cut],
590
+ delivery: Delivery,
591
+ *,
592
+ burn_subtitles: bool,
593
+ renderer: str = "ffmpeg",
594
+ remotion_workspace: Path = DEFAULT_REMOTION_WORKSPACE,
595
+ remotion_concurrency: int = DEFAULT_REMOTION_CONCURRENCY,
596
+ ) -> dict[str, Any]:
597
+ ffmpeg = _require("ffmpeg")
598
+ _require("ffprobe")
599
+ output_root = episode / OUTPUT_DIRECTORY
600
+ segments_root = output_root / SEGMENT_DIRECTORY
601
+ segments_root.mkdir(parents=True, exist_ok=True)
602
+
603
+ segments: list[Path] = []
604
+ spans: list[float] = []
605
+ for cut in cuts:
606
+ media = _resolve_media(episode, project_root, cut.media)
607
+ if media is None:
608
+ raise EditError(f"{cut.cut_id} 的素材不存在: {cut.media}")
609
+ segment = segments_root / f"{cut.cut_id}.mp4"
610
+ command = [
611
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-y",
612
+ "-ss", f"{cut.start:.3f}", "-t", f"{cut.end - cut.start:.3f}", "-i", str(media),
613
+ ]
614
+ match = _shot_match_filter(cut)
615
+ if match:
616
+ command += ["-vf", match]
617
+ command += [
618
+ "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p",
619
+ "-c:a", "aac", "-b:a", "192k", str(segment),
620
+ ]
621
+ _run(command)
622
+ segments.append(segment)
623
+ spans.append(probe_duration(segment))
624
+
625
+ with tempfile.TemporaryDirectory() as scratch:
626
+ listing = Path(scratch) / "segments.txt"
627
+ listing.write_text(
628
+ "".join(f"file '{segment.as_posix()}'\n" for segment in segments), encoding="utf-8"
629
+ )
630
+ joined = output_root / "成片-未混音.mp4"
631
+ _run([ffmpeg, "-hide_banner", "-loglevel", "error", "-y",
632
+ "-f", "concat", "-safe", "0", "-i", str(listing), "-c", "copy", str(joined)])
633
+
634
+ filters: list[str] = []
635
+ subtitle_path = None
636
+ styled_path = None
637
+ overlay_path = None
638
+ if burn_subtitles and any(cut.subtitles for cut in cuts):
639
+ canvas = probe_stream(segments[0])
640
+ cues = _subtitle_cues(cuts, spans)
641
+ subtitle_path = output_root / "字幕.srt"
642
+ subtitle_path.write_text(_build_srt(cues), encoding="utf-8")
643
+ if renderer == "remotion":
644
+ overlay_path = _render_remotion_overlay(
645
+ output_root, cues, canvas, sum(spans), remotion_workspace,
646
+ concurrency=remotion_concurrency,
647
+ )
648
+ else:
649
+ styled = styled_path = output_root / "字幕.ass"
650
+ styled.write_text(
651
+ _build_ass(cues, canvas["width"] or 1080, canvas["height"] or 1920),
652
+ encoding="utf-8",
653
+ )
654
+ escaped = (
655
+ str(styled).replace("\\", "/").replace(":", r"\:").replace("'", r"\'")
656
+ )
657
+ filters.append(f"ass='{escaped}'")
658
+
659
+ # Grain goes on last, over the whole assembled film, so one texture sits
660
+ # across every cut. `t` makes it move frame to frame — static noise reads
661
+ # as dirt on the lens, not as film.
662
+ grain = (
663
+ f"noise=alls={delivery.grain:g}:allf=t+u"
664
+ if delivery.grain is not None
665
+ else None
666
+ )
667
+ if grain is not None:
668
+ filters.append(grain)
669
+
670
+ final = output_root / "成片.mp4"
671
+ command = [ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(joined)]
672
+ if overlay_path is not None:
673
+ # VP8 carries its alpha as WebM block additions, so the decoder has
674
+ # to be named: the default one drops it and the overlay arrives as
675
+ # an opaque black rectangle.
676
+ command += ["-c:v", "libvpx", "-i", str(overlay_path)]
677
+ # The overlay branch owns its own chain, so grain is spliced in
678
+ # after the composite rather than left in `filters`, which this
679
+ # branch never reads.
680
+ chain = "[0:v][1:v]overlay=0:0:format=auto"
681
+ if grain is not None:
682
+ chain += f",{grain}"
683
+ command += [
684
+ "-filter_complex",
685
+ f"{chain},format=yuv420p[v]",
686
+ "-map", "[v]", "-map", "0:a",
687
+ "-c:v", "libx264", "-preset", "medium", "-crf", "18",
688
+ ]
689
+ elif filters:
690
+ command += ["-vf", ",".join(filters), "-c:v", "libx264",
691
+ "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p"]
692
+ else:
693
+ command += ["-c:v", "copy"]
694
+ if delivery.loudness_lufs is not None:
695
+ command += ["-af", _loudnorm_filter(ffmpeg, joined, delivery.loudness_lufs)]
696
+ command += ["-c:a", "aac", "-b:a", "192k", "-ar", "48000"]
697
+ else:
698
+ command += ["-c:a", "copy"]
699
+ command.append(str(final))
700
+ _run(command)
701
+
702
+ return {
703
+ "成片": str(final),
704
+ "分段": [str(segment) for segment in segments],
705
+ "字幕": str(subtitle_path) if subtitle_path else None,
706
+ "压制字幕": str(styled_path) if styled_path else None,
707
+ "字幕叠层": str(overlay_path) if overlay_path else None,
708
+ "字幕渲染": renderer if subtitle_path else None,
709
+ "段数": len(segments),
710
+ "各段时长之和": round(sum(cut.end - cut.start for cut in cuts), 2),
711
+ }
712
+
713
+
714
+ def _shot_match_filter(cut: Cut) -> str:
715
+ """Match one cut to its neighbours, using only what the cut list declared.
716
+
717
+ Generated shots drift: two takes of the same person at the same table come
718
+ back a stop apart and half a step of white balance away from each other, and
719
+ the join reads as a mistake rather than a cut. The correction stays a stated
720
+ creator decision — the tool never measures a clip and adjusts it on its own,
721
+ because a correction nobody wrote down is one nobody can review.
722
+ """
723
+
724
+ stages: list[str] = []
725
+ eq = [
726
+ f"{name}={cut.picture[key]}"
727
+ for key, name in (("brightness", "brightness"), ("saturation", "saturation"))
728
+ if key in cut.picture
729
+ ]
730
+ if eq:
731
+ stages.append("eq=" + ":".join(eq))
732
+ warmth = cut.picture.get("warmth")
733
+ if warmth:
734
+ # Positive is warmer: lift red, drop blue, by the same small amount.
735
+ amount = warmth / 100.0
736
+ stages.append(f"colorbalance=rm={amount:.4f}:bm={-amount:.4f}")
737
+ return ",".join(stages)
738
+
739
+
740
+ def _loudnorm_filter(ffmpeg: str, media: Path, target: float) -> str:
741
+ """Measure EBU R128 before normalization; verify the encoded output afterward.
742
+
743
+ FFmpeg can fall back to dynamic processing when linear gain would exceed
744
+ the peak or loudness-range target. Two passes do not guarantee target LUFS.
745
+ """
746
+
747
+ common = f"I={target}:TP=-1.5:LRA=11"
748
+ result = subprocess.run(
749
+ [ffmpeg, "-hide_banner", "-nostats", "-i", str(media),
750
+ "-af", f"loudnorm={common}:print_format=json", "-f", "null", "-"],
751
+ capture_output=True, text=True, check=False,
752
+ )
753
+ measured = _last_json_object(result.stderr)
754
+ required = ("input_i", "input_tp", "input_lra", "input_thresh", "target_offset")
755
+ if not measured or not all(key in measured for key in required):
756
+ # Say so rather than silently delivering a single-pass approximation.
757
+ raise EditError(
758
+ "loudnorm 第一遍没有返回可用的测量结果,无法做两遍响度标准化;"
759
+ "检查成片音轨是否为空"
760
+ )
761
+ return (
762
+ f"loudnorm={common}:measured_I={measured['input_i']}"
763
+ f":measured_TP={measured['input_tp']}:measured_LRA={measured['input_lra']}"
764
+ f":measured_thresh={measured['input_thresh']}"
765
+ f":offset={measured['target_offset']}:linear=true:print_format=summary"
766
+ )
767
+
768
+
769
+ def _sync_remotion_workspace(workspace: Path) -> Path:
770
+ """Copy the shipped composition into a runnable workspace outside the project.
771
+
772
+ Editing the composition means editing the skill's own source; the workspace
773
+ is a build directory that is rewritten on every run, so a change here can
774
+ never be quietly lost, and `node_modules` never lands inside the repository.
775
+ """
776
+
777
+ workspace = workspace.expanduser().resolve()
778
+ (workspace / "src").mkdir(parents=True, exist_ok=True)
779
+ for name in REMOTION_SOURCE_FILES:
780
+ source = REMOTION_SOURCE / name
781
+ if not source.is_file():
782
+ raise EditError(f"技能里缺少 Remotion 源文件: {name}")
783
+ shutil.copyfile(source, workspace / name)
784
+ return workspace
785
+
786
+
787
+ def _render_remotion_overlay(
788
+ output_root: Path,
789
+ cues: Sequence[tuple[float, float, str]],
790
+ canvas: dict[str, Any],
791
+ duration: float,
792
+ workspace_root: Path,
793
+ concurrency: int = DEFAULT_REMOTION_CONCURRENCY,
794
+ ) -> Path:
795
+ """Render the subtitle layer as a transparent video with Remotion.
796
+
797
+ The picture is never re-drawn by the browser: only the type is, onto an
798
+ empty frame, and ffmpeg composites that over untouched footage. So the
799
+ route costs one overlay pass and buys real typography — weight, rim, safe
800
+ area, wrapping and an entry animation — expressed once in CSS instead of in
801
+ a subtitle format whose own scaling has to be reasoned about.
802
+
803
+ Remotion is a separate project with its own licence: free for individuals
804
+ and small companies, paid above that. It is opt-in for exactly that reason,
805
+ and nothing installs it behind the creator's back.
806
+ """
807
+
808
+ workspace = _sync_remotion_workspace(workspace_root)
809
+ if not (workspace / "node_modules").is_dir():
810
+ raise EditError(
811
+ f"Remotion 还没安装。先运行一次:\n"
812
+ f" cd {workspace} && npm install\n"
813
+ "或改用默认的 ffmpeg 字幕(去掉 --subtitles remotion)。\n"
814
+ "注意 Remotion 有自己的许可证:个人与小团队免费,超出规模需要商业授权;"
815
+ "本工具不会替你安装它。"
816
+ )
817
+ npx = _which("npx")
818
+ if npx is None:
819
+ raise EditError(
820
+ "PATH 上没有 npx,无法运行 Remotion;装好 Node.js,或改用默认的 ffmpeg 字幕。"
821
+ )
822
+ props = output_root / "字幕.props.json"
823
+ props.write_text(
824
+ json.dumps(
825
+ {
826
+ "cues": [
827
+ {"start": round(start, 3), "end": round(end, 3), "text": text}
828
+ for start, end, text in cues
829
+ ],
830
+ "width": canvas["width"] or 1080,
831
+ "height": canvas["height"] or 1920,
832
+ "fps": canvas["fps"] or 24,
833
+ "durationInSeconds": round(duration, 3),
834
+ "fontScale": 0.034,
835
+ "bottomScale": 0.055,
836
+ "fontFamily": (
837
+ '"PingFang SC", "Noto Sans CJK SC", "Source Han Sans SC", '
838
+ '"Microsoft YaHei", sans-serif'
839
+ ),
840
+ },
841
+ ensure_ascii=False,
842
+ indent=2,
843
+ ),
844
+ encoding="utf-8",
845
+ )
846
+ overlay = output_root / "字幕叠层.webm"
847
+ result = subprocess.run(
848
+ [npx, "remotion", "render", REMOTION_COMPOSITION, str(overlay),
849
+ f"--props={props}", "--log=error",
850
+ f"--concurrency={max(1, concurrency)}"],
851
+ cwd=str(workspace), capture_output=True, text=True, check=False,
852
+ )
853
+ if result.returncode != 0 or not overlay.is_file():
854
+ raise EditError(
855
+ "Remotion 渲染失败:\n" + (result.stderr or result.stdout or "").strip()[-2000:]
856
+ )
857
+ return overlay
858
+
859
+
860
+ def _frame_geometry(media: Path) -> str:
861
+ stream = probe_stream(media)
862
+ return f"{stream['width']}x{stream['height']}"
863
+
864
+
865
+ def _subtitle_cues(
866
+ cuts: Sequence[Cut], spans: Sequence[float]
867
+ ) -> list[tuple[float, float, str]]:
868
+ """Place each line in output time, measuring the segments that were written.
869
+
870
+ A segment lands on a frame boundary, so it is a few milliseconds longer than
871
+ the cut list declares. Accumulating the declared numbers instead drifts —
872
+ a third of a second by the end of eight cuts here — and the subtitle leaves
873
+ before the actor stops speaking. The rendered files are the timeline.
874
+ """
875
+
876
+ cues: list[tuple[float, float, str]] = []
877
+ cursor = 0.0
878
+ for cut, span in zip(cuts, spans):
879
+ declared = cut.end - cut.start
880
+ # The windows were authored against the declared span; hold them in
881
+ # place proportionally rather than letting the tail slip out.
882
+ scale = span / declared if declared > 0 else 1.0
883
+ for window_start, window_end, text in cut.subtitles:
884
+ start = 0.0 if window_start is None else window_start
885
+ end = declared if window_end is None else window_end
886
+ cues.append((cursor + start * scale, cursor + end * scale, text))
887
+ cursor += span
888
+ return cues
889
+
890
+
891
+ def _build_srt(cues: Sequence[tuple[float, float, str]]) -> str:
892
+ """The text is the screenplay's, verbatim; only the timing is ours."""
893
+
894
+ return "\n".join(
895
+ f"{index}\n{_timecode(start)} --> {_timecode(end)}\n{text}\n"
896
+ for index, (start, end, text) in enumerate(cues, start=1)
897
+ )
898
+
899
+
900
+ def _build_ass(
901
+ cues: Sequence[tuple[float, float, str]], width: int, height: int
902
+ ) -> str:
903
+ """Author the ASS directly so the type size is stated in frame pixels.
904
+
905
+ Letting ffmpeg convert the SRT hands libass a 384-high canvas, and every
906
+ size in the style is then scaled by height/384 on the way to the frame.
907
+ Computing a FontSize from the real height on top of that scales it twice:
908
+ the first attempt here produced type a third of the frame wide, sitting in
909
+ the middle of the picture with both ends of the line cut off. Declaring
910
+ PlayRes as the frame removes the conversion, so one unit is one pixel.
911
+ """
912
+
913
+ font_size = max(18, round(height * 0.034))
914
+ outline = max(2, round(height * 0.0022))
915
+ margin_v = max(24, round(height * 0.055))
916
+ margin_h = max(24, round(width * 0.06))
917
+ header = (
918
+ "[Script Info]\n"
919
+ "ScriptType: v4.00+\n"
920
+ "WrapStyle: 2\n"
921
+ "ScaledBorderAndShadow: yes\n"
922
+ f"PlayResX: {width}\n"
923
+ f"PlayResY: {height}\n"
924
+ "\n[V4+ Styles]\n"
925
+ "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, "
926
+ "OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, "
927
+ "ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, "
928
+ "MarginL, MarginR, MarginV, Encoding\n"
929
+ f"Style: 正片,{SUBTITLE_FONT},{font_size},&H00FFFFFF,&H00FFFFFF,"
930
+ f"&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,{outline},1,2,"
931
+ f"{margin_h},{margin_h},{margin_v},1\n"
932
+ "\n[Events]\n"
933
+ "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, "
934
+ "Effect, Text\n"
935
+ )
936
+ lines = [
937
+ f"Dialogue: 0,{_ass_time(start)},{_ass_time(end)},正片,,0,0,0,,{_ass_text(text)}"
938
+ for start, end, text in cues
939
+ ]
940
+ return header + "\n".join(lines) + "\n"
941
+
942
+
943
+ def _ass_text(text: str) -> str:
944
+ """Escape a line so ASS renders its characters instead of reading them.
945
+
946
+ `{`…`}` is an override block in ASS and `\\` starts an escape, so a quoted
947
+ line that happens to contain either would lose characters on screen with no
948
+ error anywhere — and EDT-05 exists precisely to keep the burned text equal
949
+ to the screenplay's, character for character.
950
+ """
951
+
952
+ return text.replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}")
953
+
954
+
955
+ def _ass_time(seconds: float) -> str:
956
+ hundredths = int(round(seconds * 100))
957
+ hours, hundredths = divmod(hundredths, 360000)
958
+ minutes, hundredths = divmod(hundredths, 6000)
959
+ whole, hundredths = divmod(hundredths, 100)
960
+ return f"{hours:d}:{minutes:02d}:{whole:02d}.{hundredths:02d}"
961
+
962
+
963
+ def _timecode(seconds: float) -> str:
964
+ milliseconds = int(round(seconds * 1000))
965
+ hours, milliseconds = divmod(milliseconds, 3_600_000)
966
+ minutes, milliseconds = divmod(milliseconds, 60_000)
967
+ whole, milliseconds = divmod(milliseconds, 1000)
968
+ return f"{hours:02d}:{minutes:02d}:{whole:02d},{milliseconds:03d}"
969
+
970
+
971
+ def _segment_colour(ffmpeg: str, path: Path) -> Optional[tuple[float, float]]:
972
+ """Mean luma and blue-red difference of one segment, on a 0-255 scale."""
973
+
974
+ result = subprocess.run(
975
+ [ffmpeg, "-v", "error", "-i", str(path), "-vf", "fps=2,scale=96:-1",
976
+ "-pix_fmt", "rgb24", "-f", "rawvideo", "-"],
977
+ capture_output=True,
978
+ )
979
+ raw = result.stdout
980
+ if result.returncode != 0 or len(raw) < 3:
981
+ return None
982
+ count = len(raw) // 3
983
+ red = sum(raw[i * 3] for i in range(count)) / count
984
+ green = sum(raw[i * 3 + 1] for i in range(count)) / count
985
+ blue = sum(raw[i * 3 + 2] for i in range(count)) / count
986
+ return 0.299 * red + 0.587 * green + 0.114 * blue, blue - red
987
+
988
+
989
+ def _segment_colours(ffmpeg: str, output_root: Path, cuts: Sequence[Cut]) -> list[dict[str, Any]]:
990
+ """Report current cut segments individually, without imposing a shared grade."""
991
+
992
+ rows: list[dict[str, Any]] = []
993
+ for cut in cuts:
994
+ path = output_root / SEGMENT_DIRECTORY / f"{cut.cut_id}.mp4"
995
+ colour = _segment_colour(ffmpeg, path) if path.is_file() else None
996
+ row: dict[str, Any] = {"分段": path.name}
997
+ if colour is None:
998
+ row["测量"] = "未测(分段缺失或不可读)"
999
+ else:
1000
+ row.update({"平均亮度": round(colour[0], 1), "蓝减红": round(colour[1], 1)})
1001
+ rows.append(row)
1002
+ return rows
1003
+
1004
+
1005
+ def verify(episode: Path, cuts: Sequence[Cut], delivery: Delivery) -> dict[str, Any]:
1006
+ """Measure the rendered film. Every entry is a number or an honest 未测."""
1007
+
1008
+ ffmpeg = _require("ffmpeg")
1009
+ final = episode / OUTPUT_DIRECTORY / "成片.mp4"
1010
+ if not final.is_file():
1011
+ raise EditError(f"没有 {final};先运行 render")
1012
+ stream = probe_stream(final)
1013
+ expected = sum(cut.end - cut.start for cut in cuts)
1014
+ measurements: dict[str, Any] = {
1015
+ "成片": str(final),
1016
+ "实测时长": round(stream["duration"], 2),
1017
+ "各段时长之和": round(expected, 2),
1018
+ "时长差": round(stream["duration"] - expected, 2),
1019
+ "画幅": f"{stream['width']}×{stream['height']}",
1020
+ "画幅是否等于交付规格": _matches_frame_size(delivery, stream),
1021
+ "帧率": stream["fps"],
1022
+ "帧率是否等于交付规格": _matches_fps(delivery, stream),
1023
+ "目标时长": delivery.target_seconds,
1024
+ "与目标时长的差": (
1025
+ round(stream["duration"] - delivery.target_seconds, 2)
1026
+ if delivery.target_seconds is not None
1027
+ else "未测(剪辑单没有声明目标时长)"
1028
+ ),
1029
+ }
1030
+ result = subprocess.run(
1031
+ [ffmpeg, "-hide_banner", "-nostats", "-i", str(final),
1032
+ "-af", "loudnorm=print_format=json", "-f", "null", "-"],
1033
+ capture_output=True, text=True, check=False,
1034
+ )
1035
+ measured = _last_json_object(result.stderr)
1036
+ if measured and "input_i" in measured:
1037
+ measurements["实测响度 LUFS"] = float(measured["input_i"])
1038
+ measurements["实测真峰 dBTP"] = float(measured["input_tp"])
1039
+ else:
1040
+ measurements["实测响度 LUFS"] = "未测(loudnorm 没有返回可解析的测量结果)"
1041
+ measurements["交付响度目标"] = delivery.loudness_lufs
1042
+ measurements["分段色彩观测"] = _segment_colours(ffmpeg, episode / OUTPUT_DIRECTORY, cuts)
1043
+ measurements["画内可读文字"] = (
1044
+ "未测(抽有画内文字的帧,逐字对《剧本.md》的「画面文字」与提示词声明的内容)"
1045
+ )
1046
+ measurements["台词完整性"] = "未测(本工具不做转写;在成片上转写后逐句对《剧本.md》原文)"
1047
+ measurements["边界帧"] = "未测(抽剪辑点前后各一帧目视核对黑场/白场/半渲染帧)"
1048
+ return measurements
1049
+
1050
+
1051
+ def _matches_frame_size(delivery: Delivery, stream: dict[str, Any]) -> Any:
1052
+ if delivery.frame_size is None:
1053
+ return "未测(剪辑单没有声明画幅)"
1054
+ return (stream["width"], stream["height"]) == delivery.frame_size
1055
+
1056
+
1057
+ def _matches_fps(delivery: Delivery, stream: dict[str, Any]) -> Any:
1058
+ if delivery.fps is None:
1059
+ return "未测(剪辑单没有声明帧率)"
1060
+ return abs(stream["fps"] - delivery.fps) < 0.05
1061
+
1062
+
1063
+ def _last_json_object(text: str) -> Optional[dict[str, Any]]:
1064
+ """ffmpeg prints its own tail after the loudnorm block, so decode a prefix."""
1065
+
1066
+ decoder = json.JSONDecoder()
1067
+ start = text.rfind("{")
1068
+ while start != -1:
1069
+ try:
1070
+ document, _ = decoder.raw_decode(text[start:].lstrip())
1071
+ except json.JSONDecodeError:
1072
+ start = text.rfind("{", 0, start)
1073
+ continue
1074
+ if isinstance(document, dict):
1075
+ return document
1076
+ start = text.rfind("{", 0, start)
1077
+ return None
1078
+
1079
+
1080
+ def _run(command: Sequence[str]) -> None:
1081
+ result = subprocess.run(command, capture_output=True, text=True, check=False)
1082
+ if result.returncode != 0:
1083
+ raise EditError(f"命令失败: {' '.join(command[:6])}…\n{result.stderr.strip()}")
1084
+
1085
+
1086
+ def _emit(payload: dict[str, Any]) -> None:
1087
+ print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=False))
1088
+
1089
+
1090
+ def main(argv: Optional[Sequence[str]] = None) -> int:
1091
+ parser = argparse.ArgumentParser(description="短剧剪辑:核对剪辑单、渲染成片、测量成片")
1092
+ parser.add_argument("command", choices=("check", "render", "verify"))
1093
+ parser.add_argument("episode", help="剧集/<EP> 目录")
1094
+ parser.add_argument("--project-root", default=".", help="项目根目录(解析素材相对路径)")
1095
+ parser.add_argument("--no-subtitles", action="store_true", help="render 时不烧字幕")
1096
+ parser.add_argument(
1097
+ "--subtitles", choices=SUBTITLE_RENDERERS, default="ffmpeg",
1098
+ help="字幕渲染方式:ffmpeg(默认,无外部依赖)或 remotion(需先安装,排版更好)",
1099
+ )
1100
+ parser.add_argument(
1101
+ "--remotion-workspace", type=Path, default=DEFAULT_REMOTION_WORKSPACE,
1102
+ help=f"Remotion 运行工作区(默认 {DEFAULT_REMOTION_WORKSPACE}),必须在项目之外",
1103
+ )
1104
+ parser.add_argument(
1105
+ "--remotion-concurrency", type=int, default=DEFAULT_REMOTION_CONCURRENCY,
1106
+ help=(
1107
+ f"Remotion 的并发无头浏览器数(默认 {DEFAULT_REMOTION_CONCURRENCY})。"
1108
+ "每个都持有一整帧,调高很容易把内存吃满"
1109
+ ),
1110
+ )
1111
+ arguments = parser.parse_args(argv)
1112
+
1113
+ episode = Path(arguments.episode).resolve()
1114
+ project_root = Path(arguments.project_root).resolve()
1115
+ try:
1116
+ delivery, cuts, unused = parse_cut_list(episode / CUT_LIST_NAME)
1117
+ if arguments.command == "check":
1118
+ findings = check_cuts(
1119
+ episode, cuts, project_root,
1120
+ probe=_which("ffprobe") is not None, unused=unused,
1121
+ )
1122
+ payload: dict[str, Any] = {
1123
+ "段数": len(cuts),
1124
+ "各段时长之和": round(sum(cut.end - cut.start for cut in cuts), 2),
1125
+ "目标时长": delivery.target_seconds,
1126
+ "未采用镜头": unused,
1127
+ "findings": findings,
1128
+ }
1129
+ if _which("ffprobe") is None:
1130
+ payload["未测"] = ["区间是否超过素材实际时长(PATH 上没有 ffprobe)"]
1131
+ _emit(payload)
1132
+ return 1 if findings else 0
1133
+ if arguments.command == "render":
1134
+ findings = check_cuts(
1135
+ episode, cuts, project_root, probe=True, unused=unused
1136
+ )
1137
+ if findings:
1138
+ _emit({"findings": findings, "已渲染": False})
1139
+ return 1
1140
+ _emit(render(
1141
+ episode, project_root, cuts, delivery,
1142
+ burn_subtitles=delivery.burn_subtitles and not arguments.no_subtitles,
1143
+ renderer=arguments.subtitles,
1144
+ remotion_workspace=arguments.remotion_workspace,
1145
+ remotion_concurrency=arguments.remotion_concurrency,
1146
+ ))
1147
+ return 0
1148
+ _emit(verify(episode, cuts, delivery))
1149
+ return 0
1150
+ except EditError as error:
1151
+ print(str(error), file=sys.stderr)
1152
+ return 2
1153
+
1154
+
1155
+ if __name__ == "__main__":
1156
+ raise SystemExit(main())