@oh-story/dsh 0.1.8 → 0.1.9

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 (272) hide show
  1. package/README.md +11 -11
  2. package/lib/client.js +35 -14
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +165 -69
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
  8. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
  11. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  12. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  21. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  22. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
  23. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
  24. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
  25. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
  26. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
  27. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
  29. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  30. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  31. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  32. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  33. package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
  34. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
  35. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
  36. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  37. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  38. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  39. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
  40. package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
  41. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
  42. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
  43. package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
  44. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  45. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
  46. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  47. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
  50. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
  51. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
  53. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
  54. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
  55. package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
  56. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  57. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  58. package/lib/index.js +4 -2
  59. package/lib/index.js.map +2 -2
  60. package/lib/novel-to-game/VERSION +1 -1
  61. package/lib/novel-to-game/manifest.json +28 -28
  62. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
  64. package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
  65. package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
  66. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
  67. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
  68. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
  69. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
  70. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
  71. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
  72. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
  73. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
  74. package/lib/oh-story/manifest.json +292 -172
  75. package/lib/oh-story/roles/character-designer.md +1 -1
  76. package/lib/oh-story/roles/narrative-writer.md +56 -242
  77. package/lib/oh-story/roles/story-architect.md +1 -1
  78. package/lib/oh-story/skills/story/SKILL.md +1 -1
  79. package/lib/oh-story/skills/story/VERSION +1 -1
  80. package/lib/oh-story/skills/story/references/author-memory.md +10 -8
  81. package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
  82. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
  83. package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
  84. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  85. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  86. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  87. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  88. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  89. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  90. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  91. package/lib/oh-story/skills/story-import/SKILL.md +2 -2
  92. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  93. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  94. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
  95. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
  96. package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
  97. package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
  98. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
  99. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
  100. package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
  101. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  102. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  103. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
  104. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
  105. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  106. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  107. package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
  108. package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
  109. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  110. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  111. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  112. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  113. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  114. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
  115. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
  116. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
  117. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
  118. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  119. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
  120. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
  121. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  122. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  123. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
  124. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  125. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  126. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
  127. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  128. package/lib/oh-story/skills/story-review/SKILL.md +13 -3
  129. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
  130. package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
  131. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  132. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
  133. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
  134. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  135. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  136. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
  137. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  138. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  139. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  140. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  141. package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
  142. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
  143. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  144. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  145. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
  146. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  147. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  148. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  149. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  150. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  151. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
  152. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  153. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  154. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  155. package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
  156. package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
  157. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  158. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  159. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
  160. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
  161. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  162. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  163. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
  164. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  165. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  166. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
  167. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  168. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  169. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  170. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  171. package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
  172. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  173. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  174. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  175. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  176. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  177. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
  178. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  179. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  180. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  181. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  182. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  183. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  184. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  185. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  186. package/lib/types/client/index.d.ts.map +1 -1
  187. package/lib/types/client/production-prompts.d.ts.map +1 -1
  188. package/lib/types/client/production-runtime.d.ts +17 -0
  189. package/lib/types/client/production-runtime.d.ts.map +1 -1
  190. package/lib/types/skill-provider.d.ts.map +1 -1
  191. package/lib/video-recap/CHANGELOG.md +31 -1
  192. package/lib/video-recap/manifest.json +162 -162
  193. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  194. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  195. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  196. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  197. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  198. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  199. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  200. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  201. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  202. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  203. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  204. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  205. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  206. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  207. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  208. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  209. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  210. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  211. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  212. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  213. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  214. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  215. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  216. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  217. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  218. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  219. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  220. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  221. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  222. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  223. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  224. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  225. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  226. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  227. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  228. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  229. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  230. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  231. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  232. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  233. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  234. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  235. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  236. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  237. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  238. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  239. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  240. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  241. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  242. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  243. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  244. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  245. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  246. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  247. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  248. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  249. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  250. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  251. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  252. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  253. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  254. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  255. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  256. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  257. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  258. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  259. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  260. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  262. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  263. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  264. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  265. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  266. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  267. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  268. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  269. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  270. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  271. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  272. package/package.json +41 -41
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env python3
2
+ """Offline self-test for cut-list parsing and mechanical checks.
3
+
4
+ Runs without ffmpeg: every assertion here is about the document, not the media.
5
+ The media-dependent paths are exercised by the suite's tests instead.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
16
+
17
+ from edit_tool import ( # noqa: E402
18
+ DEFAULT_REMOTION_CONCURRENCY,
19
+ REMOTION_SOURCE,
20
+ REMOTION_SOURCE_FILES,
21
+ EditError,
22
+ _ass_text,
23
+ _build_ass,
24
+ _shot_match_filter,
25
+ _subtitle_cues,
26
+ check_cuts,
27
+ parse_cut_list,
28
+ )
29
+
30
+ MINIMUM_PYTHON = (3, 9)
31
+ if sys.version_info < MINIMUM_PYTHON:
32
+ raise SystemExit("selftest.py requires Python 3.9 or newer")
33
+
34
+ MOTION = """# EP001 视频提示词
35
+
36
+ ## MOTION-EP001-010 · 手指停在发布键上
37
+
38
+ ## MOTION-EP001-011 · 认证弹窗展开
39
+ """
40
+
41
+ SCREENPLAY = """# EP001
42
+
43
+ ## EP001-SC003 内 · 房间 · 夜
44
+
45
+ 角色甲:这条路我自己走。
46
+ """
47
+
48
+ CUT_LIST = """# EP001 剪辑单
49
+
50
+ - 成片目标时长:5.00 秒
51
+ - 交付响度:-16 LUFS
52
+ - 未采用镜头:MOTION-EP001-009(理由:文件缺失——尚未生产)
53
+
54
+ ## CUT-EP001-001 · 手指停在发布键上
55
+
56
+ - 来源:MOTION-EP001-010 · media/010.mp4
57
+ - 入点:0.60
58
+ - 出点:3.60
59
+ - 时长:3.00
60
+ - 取舍:入点=手指已经在下压;出点=界面开始响应
61
+ - 声音:保留原声
62
+ - 字幕:无
63
+
64
+ ## CUT-EP001-002 · 那句话
65
+
66
+ - 来源:MOTION-EP001-011 · media/011.mp4
67
+ - 入点:0.00
68
+ - 出点:2.00
69
+ - 时长:2.00
70
+ - 取舍:入点=文件起点;出点=句尾收音后
71
+ - 声音:保留原声
72
+ - 字幕:这条路我自己走
73
+ """
74
+
75
+
76
+ def require(condition: bool, message: str) -> None:
77
+ if not condition:
78
+ raise AssertionError(message)
79
+
80
+
81
+ def build(root: Path, cut_list: str) -> Path:
82
+ episode = root / "剧集" / "EP001"
83
+ (episode / "media").mkdir(parents=True)
84
+ (episode / "视频提示词.md").write_text(MOTION, encoding="utf-8")
85
+ (episode / "剧本.md").write_text(SCREENPLAY, encoding="utf-8")
86
+ (episode / "剪辑单.md").write_text(cut_list, encoding="utf-8")
87
+ for name in ("010.mp4", "011.mp4"):
88
+ (episode / "media" / name).write_bytes(b"")
89
+ return episode
90
+
91
+
92
+ def check_subtitle_geometry() -> None:
93
+ """FontSize is already in PlayRes units; scaling it by the frame twice ruins it.
94
+
95
+ Letting ffmpeg convert an SRT hands libass a 384-high canvas and every size
96
+ is then multiplied by height/384 on the way to the frame. Computing the size
97
+ from the real height on top of that produced type a third of the frame wide,
98
+ sitting in the middle of the picture with both ends of the line cut off — and
99
+ it shipped, because the render after the style change was checked with
100
+ numbers instead of a frame.
101
+ """
102
+
103
+ width, height = 768, 1344
104
+ ass = _build_ass([(1.0, 2.0, "这条路我自己走")], width, height)
105
+ require(f"PlayResX: {width}" in ass, "PlayResX 必须等于画面宽")
106
+ require(f"PlayResY: {height}" in ass, "PlayResY 必须等于画面高")
107
+ style = next(line for line in ass.splitlines() if line.startswith("Style:"))
108
+ fields = style.split(":", 1)[1].split(",")
109
+ font_size = float(fields[2])
110
+ require(
111
+ height * 0.02 <= font_size <= height * 0.06,
112
+ f"字号 {font_size} 不在画面高度的 2%–6% 之间",
113
+ )
114
+ margin_v = float(fields[21])
115
+ require(margin_v < height * 0.2, f"底边距 {margin_v} 会把字幕推离安全区")
116
+ margin_h = float(fields[19])
117
+ require(margin_h > 0, "左右边距为 0 时长句会顶到画面边缘")
118
+ require("这条路我自己走" in ass, "台词原文必须原样进 ASS")
119
+
120
+
121
+ def check_subtitle_timing() -> None:
122
+ """Cues follow the segments that were written, not the numbers in the document.
123
+
124
+ Each segment lands on a frame boundary, so it runs a few milliseconds past
125
+ its declared length. Accumulating the declared numbers drifts, and the
126
+ subtitle leaves before the actor stops speaking.
127
+ """
128
+
129
+ with tempfile.TemporaryDirectory() as scratch:
130
+ root = Path(scratch)
131
+ episode = build(root, CUT_LIST)
132
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
133
+ declared = [cut.end - cut.start for cut in cuts]
134
+ measured = [span + 0.04 for span in declared]
135
+
136
+ drifting = _subtitle_cues(cuts, declared)
137
+ 真实 = _subtitle_cues(cuts, measured)
138
+ require(len(真实) == 1, f"应有一条字幕,实际 {len(真实)}")
139
+ require(
140
+ 真实[0][0] > drifting[0][0],
141
+ "分段比声明更长时,后面的字幕必须相应后移",
142
+ )
143
+ require(
144
+ abs(真实[0][0] - (drifting[0][0] + 0.04)) < 1e-6,
145
+ "位移必须等于前面各段的实测差之和",
146
+ )
147
+
148
+
149
+ def check_shot_match() -> None:
150
+ """A declared correction is applied; an undeclared one never is.
151
+
152
+ Generated shots drift a stop apart, so the join reads as a mistake. The
153
+ correction has to be visible in the document — a tool that measured clips
154
+ and adjusted them on its own would be changing pictures nobody could review.
155
+ """
156
+
157
+ with tempfile.TemporaryDirectory() as scratch:
158
+ root = Path(scratch)
159
+ episode = build(root, CUT_LIST)
160
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
161
+ require(_shot_match_filter(cuts[0]) == "", "没写画面的段不得被改动")
162
+
163
+ listed = CUT_LIST.replace(
164
+ "- 声音:保留原声\n- 字幕:无",
165
+ "- 声音:保留原声\n- 画面:亮度 +0.06;色温 -6\n- 字幕:无", 1)
166
+ (episode / "剪辑单.md").write_text(listed, encoding="utf-8")
167
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
168
+ chain = _shot_match_filter(cuts[0])
169
+ require("brightness=0.06" in chain, f"亮度没进滤镜链: {chain}")
170
+ require("colorbalance" in chain and "rm=-0.06" in chain, f"色温没进滤镜链: {chain}")
171
+
172
+ # ffmpeg 的 eq 只接受 0..3 的饱和度;负数在文档里通过、渲染时才炸。
173
+ negative = CUT_LIST.replace(
174
+ "- 声音:保留原声\n- 字幕:无",
175
+ "- 声音:保留原声\n- 画面:饱和 -1.5\n- 字幕:无", 1)
176
+ (episode / "剪辑单.md").write_text(negative, encoding="utf-8")
177
+ try:
178
+ parse_cut_list(episode / "剪辑单.md")
179
+ except EditError as error:
180
+ require("超出允许范围" in str(error), f"负饱和度报错不对: {error}")
181
+ else:
182
+ raise AssertionError("饱和 -1.5 应当被拒绝——ffmpeg 不接受负饱和度")
183
+
184
+ # 超出范围是重新调色,不是接镜,必须挡住。
185
+ wild = CUT_LIST.replace(
186
+ "- 声音:保留原声\n- 字幕:无",
187
+ "- 声音:保留原声\n- 画面:亮度 +0.9\n- 字幕:无", 1)
188
+ (episode / "剪辑单.md").write_text(wild, encoding="utf-8")
189
+ try:
190
+ parse_cut_list(episode / "剪辑单.md")
191
+ except EditError as error:
192
+ require("超出允许范围" in str(error), f"越界报错不对: {error}")
193
+ else:
194
+ raise AssertionError("亮度 +0.9 应当被拒绝")
195
+
196
+ # 写了内容却没有可执行项,是写错了,不能静默当成不校正。
197
+ vague = CUT_LIST.replace(
198
+ "- 声音:保留原声\n- 字幕:无",
199
+ "- 声音:保留原声\n- 画面:调暖一点\n- 字幕:无", 1)
200
+ (episode / "剪辑单.md").write_text(vague, encoding="utf-8")
201
+ try:
202
+ parse_cut_list(episode / "剪辑单.md")
203
+ except EditError as error:
204
+ require("没有可执行的项" in str(error), f"含混报错不对: {error}")
205
+ else:
206
+ raise AssertionError("「调暖一点」应当被拒绝")
207
+
208
+
209
+ def check_multi_subtitle() -> None:
210
+ """A shot can carry several lines, and all of them must reach the screen.
211
+
212
+ The exchange "就是什么 / 就是少了点东西 / 少了什么" is one over-shoulder shot.
213
+ A cut list that holds only one subtitle per cut silently drops the other
214
+ two: the film plays lines that never appear, and nothing reports it.
215
+ """
216
+
217
+ with tempfile.TemporaryDirectory() as scratch:
218
+ root = Path(scratch)
219
+ episode = build(root, CUT_LIST)
220
+ (episode / "剧本.md").write_text(
221
+ SCREENPLAY + "\n江晨:就是什么。\n江晨:就是少了点东西。\n江晨:少了什么。\n",
222
+ encoding="utf-8",
223
+ )
224
+ multi = CUT_LIST.replace(
225
+ "- 字幕:这条路我自己走",
226
+ "- 字幕 1:0.10-0.60 就是什么\n"
227
+ "- 字幕 2:0.80-1.40 就是少了点东西\n"
228
+ "- 字幕 3:1.50-1.90 少了什么",
229
+ )
230
+ (episode / "剪辑单.md").write_text(multi, encoding="utf-8")
231
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
232
+ require(len(cuts[1].subtitles) == 3, f"三句应全部解析,实际 {len(cuts[1].subtitles)}")
233
+ require(cuts[1].subtitles[2][2] == "少了什么", "字幕顺序错了")
234
+ require(not check_cuts(episode, cuts, root, probe=False), "三句合法字幕不该报错")
235
+ cues = _subtitle_cues(cuts, [cut.end - cut.start for cut in cuts])
236
+ require(len(cues) == 3, f"三句应各自成为一条 cue,实际 {len(cues)}")
237
+
238
+ # 编号必须连续:跳号意味着有一句被漏掉了。
239
+ gap = CUT_LIST.replace(
240
+ "- 字幕:这条路我自己走",
241
+ "- 字幕 1:0.10-0.60 就是什么\n- 字幕 3:1.50-1.90 少了什么",
242
+ )
243
+ (episode / "剪辑单.md").write_text(gap, encoding="utf-8")
244
+ try:
245
+ parse_cut_list(episode / "剪辑单.md")
246
+ except EditError as error:
247
+ require("连续" in str(error), f"跳号报错不对: {error}")
248
+ else:
249
+ raise AssertionError("字幕编号跳号应当被拒绝")
250
+
251
+ # 两句不能同时在屏上。
252
+ overlap = CUT_LIST.replace(
253
+ "- 字幕:这条路我自己走",
254
+ "- 字幕 1:0.10-1.00 就是什么\n- 字幕 2:0.80-1.40 就是少了点东西",
255
+ )
256
+ (episode / "剪辑单.md").write_text(overlap, encoding="utf-8")
257
+ try:
258
+ parse_cut_list(episode / "剪辑单.md")
259
+ except EditError as error:
260
+ require("重叠" in str(error), f"重叠报错不对: {error}")
261
+ else:
262
+ raise AssertionError("字幕时间重叠应当被拒绝")
263
+
264
+ # 多句时每句必须自带时间,否则无从摆放。
265
+ untimed = CUT_LIST.replace(
266
+ "- 字幕:这条路我自己走",
267
+ "- 字幕 1:就是什么\n- 字幕 2:0.80-1.40 就是少了点东西",
268
+ )
269
+ (episode / "剪辑单.md").write_text(untimed, encoding="utf-8")
270
+ try:
271
+ parse_cut_list(episode / "剪辑单.md")
272
+ except EditError as error:
273
+ require("各自带时间" in str(error), f"缺时间报错不对: {error}")
274
+ else:
275
+ raise AssertionError("多句字幕缺时间应当被拒绝")
276
+
277
+
278
+ def check_stale_window() -> None:
279
+ """Regenerating a shot invalidates every subtitle window bound to it.
280
+
281
+ The windows were reverse-engineered from the old take's audio. Nothing else
282
+ notices the change: the numbers stay in range, the render still succeeds,
283
+ and the subtitle appears at a moment when nobody is speaking.
284
+ """
285
+
286
+ import os
287
+ import time
288
+
289
+ with tempfile.TemporaryDirectory() as scratch:
290
+ root = Path(scratch)
291
+ # Anchored on the field, not on the line's wording: the fixture's dialogue
292
+ # is illustrative and gets rewritten, and a test that breaks when an
293
+ # example changes is a test nobody trusts.
294
+ timed = re.sub(
295
+ r"^(- 字幕:(?!无).+)$",
296
+ r"\1\n- 字幕时间:0.30-1.60",
297
+ CUT_LIST,
298
+ count=1,
299
+ flags=re.MULTILINE,
300
+ )
301
+ require(timed != CUT_LIST, "夹具里应当有一条带台词的字幕")
302
+ episode = build(root, timed)
303
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
304
+ require(
305
+ not check_cuts(episode, cuts, root, probe=False),
306
+ "素材不比剪辑单新时不该报陈旧",
307
+ )
308
+
309
+ media = episode / "media" / "011.mp4"
310
+ listing = episode / "剪辑单.md"
311
+ newer = listing.stat().st_mtime + 60
312
+ os.utime(media, (newer, newer))
313
+ findings = check_cuts(episode, cuts, root, probe=False)
314
+ require(
315
+ any("素材比剪辑单新" in item for item in findings),
316
+ f"重出后的陈旧字幕时间没抓到: {findings}",
317
+ )
318
+
319
+ # 改过剪辑单之后就不再报——那正是「重测并改」的动作。
320
+ time.sleep(0.01)
321
+ listing.write_text(timed, encoding="utf-8")
322
+ later = newer + 60
323
+ os.utime(listing, (later, later))
324
+ _, cuts, _ = parse_cut_list(listing)
325
+ require(
326
+ not any("素材比剪辑单新" in item for item in check_cuts(episode, cuts, root, probe=False)),
327
+ "改过剪辑单之后不该继续报陈旧",
328
+ )
329
+
330
+
331
+ def check_ass_escaping() -> None:
332
+ """A quoted line keeps every character it had in 剧本.md.
333
+
334
+ `{`…`}` is an override block in ASS: unescaped, libass drops the braces and
335
+ everything between them, on screen, with no error anywhere. EDT-05 exists to
336
+ keep the burned text equal to the screenplay's character for character.
337
+ """
338
+
339
+ line = "他念出来:{姓名},请签字。"
340
+ escaped = _ass_text(line)
341
+ require("\\{" in escaped and "\\}" in escaped, f"花括号没有转义: {escaped}")
342
+ require("姓名" in escaped, "转义把字弄丢了")
343
+ ass = _build_ass([(1.0, 2.0, line)], 1080, 1920)
344
+ dialogue = [row for row in ass.splitlines() if row.startswith("Dialogue")][0]
345
+ require(dialogue.endswith(escaped), f"Dialogue 行没有用转义后的正文: {dialogue}")
346
+ require(
347
+ _ass_text("反斜杠 \\N 不是换行").count("\\\\") == 1,
348
+ "反斜杠没有转义,\\N 会被当成换行",
349
+ )
350
+
351
+
352
+ def check_remotion_sources_all_shipped() -> None:
353
+ """The sync list is a whitelist; a file left out of it is missing at render.
354
+
355
+ Nothing else notices until someone has already paid for that render.
356
+ """
357
+
358
+ # `as_posix`, because the whitelist is written with forward slashes and
359
+ # `relative_to` yields backslashes on Windows.
360
+ on_disk = {
361
+ path.relative_to(REMOTION_SOURCE).as_posix()
362
+ for path in REMOTION_SOURCE.rglob("*")
363
+ if path.is_file()
364
+ and path.suffix in {".ts", ".tsx", ".json"}
365
+ and "node_modules" not in path.parts
366
+ }
367
+ listed = set(REMOTION_SOURCE_FILES)
368
+ require(not (on_disk - listed), f"Remotion 源文件没进同步清单: {sorted(on_disk - listed)}")
369
+ require(not (listed - on_disk), f"同步清单里有不存在的文件: {sorted(listed - on_disk)}")
370
+
371
+
372
+ def check_remotion_concurrency_is_capped() -> None:
373
+ """Remotion defaults to one browser per core, each holding a full frame."""
374
+
375
+ require(DEFAULT_REMOTION_CONCURRENCY >= 1, "并发上限必须是正数")
376
+ source = Path(__file__).resolve().with_name("edit_tool.py").read_text(encoding="utf-8")
377
+ require("--concurrency=" in source, "render 必须显式传 --concurrency,不能用 Remotion 的默认值")
378
+
379
+
380
+ def main() -> int:
381
+ with tempfile.TemporaryDirectory() as scratch:
382
+ root = Path(scratch)
383
+ episode = build(root, CUT_LIST)
384
+ delivery, cuts, unused = parse_cut_list(episode / "剪辑单.md")
385
+ require(delivery.target_seconds == 5.0, "目标时长没有解析出来")
386
+ require(delivery.loudness_lufs == -16.0, "交付响度没有解析出来")
387
+ require(len(cuts) == 2, f"应解析出 2 段,实际 {len(cuts)}")
388
+ require(len(unused) == 1, "未采用镜头没有解析出来")
389
+ require(cuts[1].subtitles[0][2] == "这条路我自己走", "字幕文字没有解析出来")
390
+
391
+ clean = check_cuts(episode, cuts, root, probe=False)
392
+ require(not clean, f"完好的剪辑单不该有 findings: {clean}")
393
+
394
+ # 出点 - 入点 与声明时长不符必须抓到。
395
+ drifted = CUT_LIST.replace("- 时长:3.00", "- 时长:2.50")
396
+ (episode / "剪辑单.md").write_text(drifted, encoding="utf-8")
397
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
398
+ findings = check_cuts(episode, cuts, root, probe=False)
399
+ require(any("与「时长" in item for item in findings), f"时长漂移没抓到: {findings}")
400
+
401
+ # 字幕必须能在剧本里找到原文;转写猜出来的字不行。
402
+ invented = CUT_LIST.replace("字幕:这条路我自己走", "字幕:这条陆我自己走")
403
+ (episode / "剪辑单.md").write_text(invented, encoding="utf-8")
404
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
405
+ findings = check_cuts(episode, cuts, root, probe=False)
406
+ require(any("找不到原文" in item for item in findings), f"编造的字幕没抓到: {findings}")
407
+
408
+ # 未知 MOTION 必须抓到。
409
+ unknown = CUT_LIST.replace("MOTION-EP001-011", "MOTION-EP001-099")
410
+ (episode / "剪辑单.md").write_text(unknown, encoding="utf-8")
411
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
412
+ findings = check_cuts(episode, cuts, root, probe=False)
413
+ require(any("不在《视频提示词.md》中" in item for item in findings), f"未知 MOTION 没抓到: {findings}")
414
+
415
+ # 素材缺失必须抓到。
416
+ (episode / "media" / "011.mp4").unlink()
417
+ (episode / "剪辑单.md").write_text(CUT_LIST, encoding="utf-8")
418
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
419
+ findings = check_cuts(episode, cuts, root, probe=False)
420
+ require(any("素材不存在" in item for item in findings), f"缺素材没抓到: {findings}")
421
+
422
+ # 同一素材上的两段不得重叠。
423
+ overlapping = CUT_LIST.replace(
424
+ "- 来源:MOTION-EP001-011 · media/011.mp4", "- 来源:MOTION-EP001-011 · media/010.mp4"
425
+ ).replace("- 入点:0.00", "- 入点:1.00").replace("- 出点:2.00", "- 出点:3.00")
426
+ (episode / "media" / "011.mp4").write_bytes(b"")
427
+ (episode / "剪辑单.md").write_text(overlapping, encoding="utf-8")
428
+ _, cuts, _ = parse_cut_list(episode / "剪辑单.md")
429
+ findings = check_cuts(episode, cuts, root, probe=False)
430
+ require(any("区间重叠" in item for item in findings), f"同源重叠没抓到: {findings}")
431
+
432
+ # 缺字段是文档缺陷,报出来而不是崩掉。
433
+ (episode / "剪辑单.md").write_text(
434
+ CUT_LIST.replace("- 出点:3.60\n", ""), encoding="utf-8"
435
+ )
436
+ try:
437
+ parse_cut_list(episode / "剪辑单.md")
438
+ except EditError as error:
439
+ require("出点" in str(error), f"缺字段的报错没点名字段: {error}")
440
+ else:
441
+ raise AssertionError("缺「出点」应当报错")
442
+
443
+ check_subtitle_geometry()
444
+ check_subtitle_timing()
445
+ check_shot_match()
446
+ check_ass_escaping()
447
+ check_multi_subtitle()
448
+ check_stale_window()
449
+ check_remotion_sources_all_shipped()
450
+ check_remotion_concurrency_is_capped()
451
+ print("short-drama-edit self-tests passed")
452
+ return 0
453
+
454
+
455
+ if __name__ == "__main__":
456
+ raise SystemExit(main())
@@ -2,6 +2,11 @@
2
2
 
3
3
  此文件由已接受规格生成。元信息用于核对来源;只复制引用块内的通用提示词。不要手写供应商参数。
4
4
 
5
+ **这份模板属于结构化管线**(`image-prompt-specs.jsonl` 一路)。creator-first 项目的《图片提示词.md》骨架以 `SKILL.md` 为准:`## IMG-... · 中文名`,字段写成不加粗的
6
+ `- 参考:`(取「无外部参考」或完整 `REF-...` 语法),正文标题是 `### 可复制提示词`。
7
+ 不要把这里的加粗字段名、`参考图用途`、`本次呈现` 或 `### 可复制通用提示词` 搬进 creator-first 文档——
8
+ `creator_markdown_check.py` 按 `SKILL.md` 的骨架校验,照本模板写会被判「缺少参考字段」。
9
+
5
10
  ```markdown
6
11
  # EP<编号> · 资产图片提示词
7
12
 
@@ -4,6 +4,9 @@
4
4
  只复制引用块内的自然语言提示词;本文件不触发图片或媒体服务,实际生产交
5
5
  `$short-drama-produce` 并单独确认。
6
6
 
7
+ **这是项目级文件,不是某一集的创作文档。** 它不参与 `creator_markdown_check.py` 的
8
+ 五文档校验;某一集要用的资产提示词写进该集的《图片提示词.md》,骨架以 `SKILL.md` 为准。
9
+
7
10
  ```markdown
8
11
  # 项目 Look Development 提示词
9
12
 
@@ -69,13 +69,13 @@
69
69
  | 指标 | 参考值 | 算法 | 不达标怎么办 |
70
70
  |---|---|---|---|
71
71
  | 归属置信 | ≥ 0.85 | 有明确归属的情节点 / 单元内情节点总数 | 低于线的单元标「待复核」,不删 |
72
+ | 覆盖率 | 85%–95% | 已归类情节点 / 总情节点数 | < 85% 走散落兜底;> 95% 复核是否过度归并 |
73
+ | 重叠率 | ≤ 35% | 跨单元共享的情节点 / 总情节点数 | > 35% 说明两个单元其实是一个,考虑合并 |
72
74
 
73
75
  「有明确归属」指这条情节点服务于本单元的戏剧目标或对抗力量,而不只是恰好落在本单元的
74
76
  章节范围内。按连续章节切分时,若把「落在范围内」当作归属,这个比值恒等于 1,指标失去
75
77
  意义——所以逐条判的是**它推动的是不是这个单元的那件事**,跨单元共享的情节点计入重叠率
76
78
  而不计入本单元的分子。
77
- | 覆盖率 | 85%–95% | 已归类情节点 / 总情节点数 | < 85% 走散落兜底;> 95% 复核是否过度归并 |
78
- | 重叠率 | ≤ 35% | 跨单元共享的情节点 / 总情节点数 | > 35% 说明两个单元其实是一个,考虑合并 |
79
79
 
80
80
  **小体量素材的例外**:不足 30 章、单主线连续叙事的材料按时序切分时,覆盖率天然接近
81
81
  100%、散落接近 0,这是线性结构的正常结果,不是过度归并。此时复核的重点是
@@ -103,9 +103,12 @@ P=$(grep -cE '^P[0-9]+ ' "$F")
103
103
  grep -cE '^P[0-9]+ [^|]+\|[^|]*[^|[:space:]][^|]*\|[^|]*涉及' "$F" # 必须 == $P
104
104
  grep -cE '^P[0-9]+ .*\|.*\|.*\|[^|]*功能:' "$F" # 必须 == $P
105
105
  grep -hoE '^P[0-9]+ [^|]+' "$F" | awk '{print $2}' | sort -u # 必须 ⊆ 类型表
106
- grep -cE '^\|[^|]*\|[^|]*\|' "$F" # 必须 >= 2
106
+ awk '/^## 载体记录/{f=1;next} /^## /{f=0} f&&/^\|/{n++} END{print n+0}' "$F" # 必须 >= 3
107
107
  ```
108
108
 
109
+ - 载体表计数少于 3:该章只写了表头没写内容。阈值是 3 不是 2,因为表头行与 `|---|` 分隔行
110
+ 本身就占 2 行——按 2 计的话,一张空表照样过闸。**这一条也必须只数「载体记录」小节内的
111
+ 表格行**:数全文的 `|` 行会把「出场人物」表算进来,同样让空载体表蒙混过关。
109
112
  - 白描段计数少于 `$P`:有情节点缺白描,或字段顺序、分隔符不对。
110
113
  - 功能段计数少于 `$P`:有情节点没回答改变了什么。注意全角冒号——半角会漏。
111
114
  这一条只数情节点行第四段里的 `功能:`:早先的写法数的是全文任何位置的这四个字符,
@@ -75,11 +75,13 @@ python3 <本技能目录>/scripts/production_tool.py prepare <project> --job <jo
75
75
  python3 <本技能目录>/scripts/production_tool.py confirm <project> --job-id <id> --confirmation "CONFIRM <id> <code>"
76
76
  python3 <本技能目录>/scripts/production_tool.py run <project> --job-id <id> --adapter-config <outside-project-config.json>
77
77
  python3 <本技能目录>/scripts/production_tool.py status <project> --job-id <id>
78
+ python3 <本技能目录>/scripts/production_tool.py collect <project> --job-id <id> --adapter-config <outside-project-config.json>
78
79
  python3 <本技能目录>/scripts/production_tool.py audit <project>
79
80
  ```
80
81
 
81
82
  `prepare` 只验证并预览,不生产。`confirm` 只保存与当前 job 指纹绑定的一次性确认。
82
- `run` 才启动 adapter。`audit` 只对账本地任务历史、失败后恢复、重复内容尝试和当前输出字节,
83
+ `run` 才启动 adapter。`collect` 只取回**已经提交过、已经计费**的那次任务的结果,
84
+ 不重新提交、也不需要新的确认——见下面「中断不等于要重跑」。`audit` 只对账本地任务历史、失败后恢复、重复内容尝试和当前输出字节,
83
85
  不会调用供应商,也不把技术成功、文件存在或哈希一致写成媒体质量结论。同一 job 存在未决
84
86
  `running` attempt 时禁止重新 prepare、confirm 或 run;先等待完成或排查遗留 attempt。
85
87
 
@@ -97,8 +99,8 @@ python3 <本技能目录>/scripts/production_tool.py audit <project>
97
99
  - **tts**:从 `剧本.md` 读取原句与表演要求,声音参考由用户或现有媒体明确提供。不得在生产 job
98
100
  中改词,也不为 TTS 新建第六份创作文档。
99
101
  - **music**:读取 `视频提示词.md` 中创作者已确认的时间线音乐章节;主题曲使用已确认歌词,纯配乐
100
- 不携带歌词。供应商不能精确承诺时长时,生成源音轨后仍由剪辑按文档里的混音意图完成落点、循环、
101
- 淡入淡出和对白 ducking。
102
+ 不携带歌词。供应商不能精确承诺时长时,生成源音轨后仍由 `$short-drama-edit` 按文档里的混音意图完成落点、
103
+ 循环、淡入淡出和对白 ducking。
102
104
 
103
105
  一个 job 不混合 modality。大批量工作拆成创作者能看清数量和成本边界的小 job;不为方便把整季
104
106
  隐式塞进一次确认。
@@ -133,6 +135,23 @@ adapter 配置必须在项目外,只包含 argv 命令和超时;凭据由 ad
133
135
 
134
136
  仓库自带 `fixture_adapter.py` 只用于离线测试,不代表真实生成质量或默认生产 adapter。
135
137
 
138
+ ## 中断不等于要重跑
139
+
140
+ 视频任务在**提交那一刻**就已经计费,不是在拿到结果时。提交之后的一切——轮询几分钟、下载——
141
+ 都可能被进程被杀、断网、机器休眠打断。内置 adapter 因此在拿到供应商任务 ID 的第一时间就把它
142
+ 写进 `handle_path`(早于第一次轮询),这个路径不随本次尝试一起删除。
143
+
144
+ 于是中断之后有三条确定的动作,不必再花一次钱:
145
+
146
+ 1. `audit` 会把带着任务 ID 的未完成尝试报成 `orphaned_provider_job`,
147
+ `action` 是 `collect_before_retry`;
148
+ 2. `collect` 用那个 ID 取回结果并把这次尝试标成成功;
149
+ 3. 只有在 `collect` 也确认那边确实失败之后,才走重新确认与重投的老路。
150
+
151
+ **不要在 `audit` 报出 `orphaned_provider_job` 时直接重投**——那是在为同一个镜头付第二次钱。
152
+ `collect` 不走确认闸门是有意的:闸门防的是意外花钱,而 collect 不花钱;
153
+ 如果它也要求重新确认,那么中断之后最省事的路径就变成再付一次,正好是闸门要防的事。
154
+
136
155
  ## 结果与复核
137
156
 
138
157
  成功后回报实际输出路径、媒体类型和运行状态;不要把“adapter 返回成功”写成质量结论。
@@ -145,6 +164,8 @@ adapter 配置必须在项目外,只包含 argv 命令和超时;凭据由 ad
145
164
  构图与角色一致、画面本身合规的图;音频被拒重录那句台词。改动写进新的 job 重新 prepare,让创
146
165
  作者在预览里看到改的是哪一项再确认;原样重投的那次确认不产生修复,只产生一笔费用。重复内容
147
166
  缺陷回到对应 prompt/spec owner。
167
+ 生产结束后不自动进入剪辑。素材要装配成成片时由用户点名 `$short-drama-edit`;它只取舍已有帧,
168
+ 不回头改本阶段的 job,也不生成新素材。
148
169
  如需质量复核,报告可把已有结果另行交给 `$short-drama-review`;不要在生产调用中自动启动复核。
149
170
  Dashboard 只负责展示这些文件和运行摘要,不提供 adapter 设置或生产按钮。
150
171
 
@@ -113,6 +113,34 @@ on the machine locale. The document contains the confirmed job plus:
113
113
  production tool. Every adapter output source must be a direct regular-file
114
114
  child of this directory. The whole directory is deleted after success or
115
115
  failure.
116
+ - `handle_path`: absolute path where the adapter records the provider's task id
117
+ **as soon as it has one, before it starts polling** — write
118
+ `{"provider_job_id": "<id>"}`, atomically. Unlike `output_root` this path
119
+ survives the attempt.
120
+ - `collect_provider_job_id`: present only on a collect call. When it is set the
121
+ adapter must **not submit anything**; it polls and downloads that existing
122
+ task and returns its outputs as usual.
123
+
124
+ ## Why the handle exists
125
+
126
+ A video task is billed the moment it is submitted, not when its result is
127
+ collected. Everything after submission — polling for minutes, downloading — can
128
+ be interrupted by a killed process, a dropped connection, or a sleeping laptop.
129
+ Without a durable id the attempt record says `failed` while the provider's task
130
+ is alive and already paid for, and the only way forward is to submit again and
131
+ pay a second time for the same shot.
132
+
133
+ So: write the handle before the first poll, and treat failing to write it as
134
+ non-fatal — a submitted task must never be failed because its id could not be
135
+ recorded locally. The tool copies the handle onto the attempt record on both the
136
+ success and the failure path, `audit` reports any unfinished attempt that
137
+ carries one as `orphaned_provider_job` with `action: collect_before_retry`, and
138
+ `production_tool.py collect` fetches it.
139
+
140
+ `collect` deliberately sits outside the confirmation gate. That gate exists to
141
+ prevent an unintended charge; collecting spends nothing because the charge
142
+ already happened. Requiring a fresh confirmation would make paying again the
143
+ cheapest way out of an interruption — the exact outcome the gate is for.
116
144
 
117
145
  It may translate provider-neutral parameters into its chosen SDK/API. Optional
118
146
  provider adapters under `scripts/` document and implement known translations;
@@ -31,6 +31,12 @@ required and are only accepted when the runtime profile above permits their valu
31
31
  optional for reference-conditioned jobs and **required** for text-to-video, where `adaptive` is
32
32
  refused because there is no reference frame to adapt to.
33
33
 
34
+ The adapter validates and forwards `duration`; it does not estimate speech length or retime dialogue.
35
+ Before preparing a speaking shot, compare the accepted shot duration, the prompt's spoken timeline,
36
+ and the job's `duration`. Resolve insufficient speaking time in the storyboard and regenerate the
37
+ dependent prompt before preparing the job. Prompt timestamps alone do not change the request duration.
38
+ The official video API has no separate speech-rate parameter.
39
+
34
40
  The prompt is compiled into one `text` item of the multimodal `content` array and is refused above
35
41
  7000 characters. Each declared reference becomes one further `content` item carrying an explicit
36
42
  `role`: `first_frame`, `last_frame`, `reference_image`, `reference_video`, or `reference_audio`.
@@ -73,11 +79,24 @@ what changes upstream is only which axes of the target-model profile a project d
73
79
  video-prompt skill's target-model profile. Two consequences are worth stating here because they
74
80
  show up as production defects rather than as API errors:
75
81
 
76
- - an unstated audio channel is not a silent one. A shot that says nothing about music or speech can
77
- come back with invented score and invented lines, so the shot's own sound intent, including what
78
- must **not** be produced, has to be in the copyable body;
79
- - readable on-screen text is likewise produced unless it is excluded, so a shot with no readable-text
80
- obligation states that exclusion instead of relying on the model to omit it.
82
+ - an unstated **music** channel is not a silent one. Probing this suite's own prompts against H3, a
83
+ body carrying `non_diegetic_music: N/A` came back without a score in 14 of 14 runs, while a body
84
+ that said nothing about music came back with an invented score in 2 of 3. State the music intent.
85
+ - the **speech** channel did not behave the same way in those probes: no run invented dialogue,
86
+ including 8 runs whose body carried a contradictory `No dialogue` exclusion. Treat "an unstated
87
+ vocal channel gets filled" as unverified rather than as a known failure, and write the shot's
88
+ sound intent because the body should say what this shot sounds like — not on the strength of a
89
+ mechanism this suite has not observed.
90
+ - readable on-screen text was **not** produced unmasked in those probes either: across 44 sampled
91
+ frames from 11 clips — including dialogue clips carrying no exclusion sentence, on H3 and on
92
+ Seedance 2.0/2.5 — no burned-in subtitle or caption appeared. The exclusion sentence this suite
93
+ mandates is therefore precautionary **for video** rather than evidence-driven. Keep writing it:
94
+ a one-line exclusion costs nothing, these probes are small enough that they only show the failure
95
+ was not produced here, and generator behaviour changes between versions. What should change is the
96
+ justification — do not defend it as a fix for an observed defect. Note the asymmetry before carrying this reasoning anywhere else:
97
+ the same probe run against `gpt-image-2` found the opposite — an unconstrained still came back
98
+ covered in invented readable text, and the exclusion sentence cleaned it up. The image stages
99
+ keep their text policy on evidence; only the video stage's copy of it is precautionary.
81
100
 
82
101
  Neither is written here as a fixed phrase, and this suite never injects one. The wording belongs to
83
102
  the shot and to the project's declared prompt language.