@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
@@ -94,8 +94,14 @@ SPOKEN_RUN_RE = re.compile(r"[\u3400-\u9fff]{4,}")
94
94
  EXPLICIT_TEXT_TO_VIDEO = "无(创作者已明确选择文生视频)"
95
95
  PENDING_REFERENCE_SUFFIX_RE = re.compile(r";待补参考图:[^;。\n]+。?$")
96
96
  VISUAL_CATEGORIES = ("人物", "造型", "地点", "道具")
97
+ # A malformed entry always tries to name something; a heading that is only the
98
+ # category word is a section divider ("## 人物"), and rejecting it left the
99
+ # document with no way to group entries at all.
97
100
  VISUAL_SETTING_LINE_RE = re.compile(
98
- r"^#{2,4}[ \t ]*(?:" + "|".join(VISUAL_CATEGORIES) + r")[^\n]*$", re.MULTILINE
101
+ r"^#{2,4}[ \t ]*(?:"
102
+ + "|".join(VISUAL_CATEGORIES)
103
+ + r")(?![ \t ]*$)[^\n]*$",
104
+ re.MULTILINE,
99
105
  )
100
106
  VISUAL_SETTING_HEADING_RE = re.compile(
101
107
  r"^## (" + "|".join(VISUAL_CATEGORIES) + r") · (.+?)[ \t ]*$", re.MULTILINE
@@ -175,6 +181,10 @@ def _fields(section: str, *, owner: str, errors: list[str]) -> dict[str, str]:
175
181
  pairs = re.findall(r"^- ([^:\n]+):(.+)$", section, re.MULTILINE)
176
182
  fields: dict[str, str] = {}
177
183
  for key, value in pairs:
184
+ # `- **参考**:…` is ordinary Markdown and reads as the same field to a
185
+ # human. Taking the emphasis literally turned it into a different key,
186
+ # so the author got "缺少参考字段" while looking straight at 参考.
187
+ key = re.sub(r"^(?:\*\*|__|\*|_)(.+?)(?:\*\*|__|\*|_)$", r"\1", key.strip())
178
188
  if key in fields:
179
189
  errors.append(f"{owner}: 字段重复: {key}")
180
190
  fields[key] = value
@@ -242,6 +252,43 @@ def _copyable_prompt(
242
252
  return prompt or None
243
253
 
244
254
 
255
+ def _copyable_prompt_cause(
256
+ section: str, heading: str = r"可复制(?:通用)?提示词"
257
+ ) -> str:
258
+ """Why `_copyable_prompt` returned None, as a suffix for the error.
259
+
260
+ "缺少唯一且非空的可复制提示词" is true of four different documents and tells
261
+ the author nothing about which one they wrote. The expensive case is a
262
+ single stray line -- a separator, an HTML comment, a note -- inside an
263
+ otherwise complete block: the prompt is visibly right there, so the message
264
+ reads as a checker bug and the real line goes unlooked at.
265
+ """
266
+
267
+ # Name the heading the author actually has to write, not this one's default:
268
+ # the keyframe caller passes a different one, and pointing at the wrong
269
+ # heading is worse than saying nothing.
270
+ label = "### " + re.sub(r"\(\?:([^)]*)\)\?", "", heading).replace("\\", "")
271
+ markers = list(re.finditer(rf"^### {heading}\s*$", section, re.MULTILINE))
272
+ if not markers:
273
+ return f":没有 `{label}` 小节标题"
274
+ if len(markers) > 1:
275
+ return f":`{label}` 出现了 {len(markers)} 次,只能有一个"
276
+ body = section[markers[0].end() :]
277
+ following = re.search(r"^###\s+|^##\s+", body, re.MULTILINE)
278
+ if following is not None:
279
+ body = body[: following.start()]
280
+ lines = [line for line in body.splitlines() if line.strip()]
281
+ if not lines:
282
+ return f":`{label}` 下面是空的"
283
+ intruders = [line for line in lines if not line.startswith(">")]
284
+ if intruders:
285
+ return (
286
+ ":小节内有不以 `>` 开头的行,整块因此不算引用块——"
287
+ f"把它移到小节外,或删掉:{_excerpt(intruders[0])}"
288
+ )
289
+ return ":引用块里只有空白"
290
+
291
+
245
292
  def _portable_path(value: str) -> bool:
246
293
  if not value or "\\" in value or re.match(r"^[A-Za-z]:", value):
247
294
  return False
@@ -566,7 +613,11 @@ def _visual_entries(document: str, errors: list[str]) -> list[VisualEntry]:
566
613
 
567
614
 
568
615
  def _named_entries(
569
- prompt: str, entries: list[VisualEntry], *, fold_case: bool = False
616
+ prompt: str,
617
+ entries: list[VisualEntry],
618
+ *,
619
+ fold_case: bool = False,
620
+ by_entry_name: bool = False,
570
621
  ) -> set[tuple[str, str]]:
571
622
  """Which declared entries does this frozen keyframe actually call by name?
572
623
 
@@ -578,7 +629,16 @@ def _named_entries(
578
629
  haystack = haystack.casefold()
579
630
  owners: dict[str, list[VisualEntry]] = {}
580
631
  for entry in entries:
581
- for designator in entry.designators:
632
+ # 来源 quotes 剧本.md, which is written in the project's own language and
633
+ # calls people by their entry names — not by the 画面代称 a prompt body
634
+ # uses. Matching a Chinese screenplay quote against an English designator
635
+ # would never fire, so that text is matched by entry name instead. An
636
+ # entry that opted out with `画面代称:无` opted out of being found by
637
+ # name at all, and stays out of both.
638
+ needles = (
639
+ [entry.name] if by_entry_name and entry.designators else entry.designators
640
+ )
641
+ for designator in needles:
582
642
  needle = re.sub(r"\s+", " ", designator).strip()
583
643
  if fold_case:
584
644
  needle = needle.casefold()
@@ -665,13 +725,17 @@ def _check_named_coverage(
665
725
  basis: "VisualBasis",
666
726
  entries: list[VisualEntry],
667
727
  errors: list[str],
728
+ *,
729
+ by_entry_name: bool = False,
668
730
  ) -> None:
669
731
  """Every entry this text calls by name is either in frame or declared 画外."""
670
732
  # Matching is case-sensitive so an ordinary English word never impersonates a
671
733
  # character called May or Will. A body that writes the name in another case
672
734
  # would otherwise fall out of the check silently, so it is reported here.
673
- named = _named_entries(prompt, entries)
674
- folded = _named_entries(prompt, entries, fold_case=True)
735
+ named = _named_entries(prompt, entries, by_entry_name=by_entry_name)
736
+ folded = _named_entries(
737
+ prompt, entries, fold_case=True, by_entry_name=by_entry_name
738
+ )
675
739
  for category, name in sorted(folded - named):
676
740
  errors.append(
677
741
  f"{owner}: {where}里的名字与画面代称大小写不一致: {category}「{name}」;"
@@ -870,6 +934,17 @@ def _check_language_designators(
870
934
  )
871
935
 
872
936
 
937
+ def _declared_seconds(value: str) -> Optional[float]:
938
+ """The number of seconds a 时长 field declares, however it is spelled.
939
+
940
+ `4s`, `4 秒` and `4秒` are the same duration. Returning None for anything
941
+ else keeps an unparseable value out of the comparison instead of turning a
942
+ formatting difference into a false duration conflict.
943
+ """
944
+ match = re.search(r"(\d+(?:\.\d+)?)", value or "")
945
+ return float(match.group(1)) if match else None
946
+
947
+
873
948
  def _check_continuity_locks(
874
949
  locks: list[ContinuityLock],
875
950
  *,
@@ -1091,7 +1166,10 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1091
1166
  image_prompt = _copyable_prompt(body)
1092
1167
  image_prompts[match.group(1)] = image_prompt
1093
1168
  if image_prompt is None:
1094
- errors.append(f"{match.group(1)}: 缺少唯一且非空的可复制提示词")
1169
+ errors.append(
1170
+ f"{match.group(1)}: 缺少唯一且非空的可复制提示词"
1171
+ + _copyable_prompt_cause(body)
1172
+ )
1095
1173
 
1096
1174
  motions = _sections(video, "MOTION")
1097
1175
  shot_ids = re.findall(r"^## (SHOT-[A-Z0-9-]+)\b", storyboard, re.MULTILINE)
@@ -1122,9 +1200,11 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1122
1200
  errors.append("视频提示词.md: 没有 MOTION 条目")
1123
1201
 
1124
1202
  motion_by_shot: dict[str, tuple[str, str, Optional[str]]] = {}
1203
+ motion_duration: dict[str, str] = {}
1125
1204
  for motion_id, body in motions.items():
1126
1205
  fields = _fields(body, owner=motion_id, errors=errors)
1127
1206
  shot_id = _plain(fields.get("分镜", ""))
1207
+ motion_duration[shot_id] = _plain(fields.get("时长", ""))
1128
1208
  copyable_prompt = _copyable_prompt(body)
1129
1209
  if not shot_id:
1130
1210
  errors.append(f"{motion_id}: 缺少分镜字段")
@@ -1135,16 +1215,34 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1135
1215
  if motion_id.removeprefix("MOTION-") != shot_id.removeprefix("SHOT-"):
1136
1216
  errors.append(f"{motion_id}: ID 必须与分镜 {shot_id} 一一对应")
1137
1217
  if copyable_prompt is None:
1138
- errors.append(f"{motion_id}: 缺少唯一且非空的可复制提示词")
1218
+ errors.append(
1219
+ f"{motion_id}: 缺少唯一且非空的可复制提示词"
1220
+ + _copyable_prompt_cause(body)
1221
+ )
1139
1222
 
1140
1223
  if set(motion_by_shot) != set(shots):
1141
1224
  errors.append("分镜.md/视频提示词.md: SHOT 与 MOTION 未一一对应")
1142
1225
 
1143
1226
  for shot_id, shot_body in shots.items():
1144
1227
  fields = _fields(shot_body, owner=shot_id, errors=errors)
1228
+ source_value = fields.get("来源", "")
1145
1229
  claimed_scenes.update(
1146
- _shot_sources(fields.get("来源", ""), shot_id, scenes, errors)
1230
+ _shot_sources(source_value, shot_id, scenes, errors)
1147
1231
  )
1232
+ shot_seconds = _declared_seconds(_plain(fields.get("时长", "")))
1233
+ motion_seconds = _declared_seconds(motion_duration.get(shot_id, ""))
1234
+ if (
1235
+ shot_seconds is not None
1236
+ and motion_seconds is not None
1237
+ and shot_seconds != motion_seconds
1238
+ ):
1239
+ # 时长 is the value actually sent to the execution end and the term
1240
+ # VID-04/VID-13 arithmetic is built on. A downstream copy that drifts
1241
+ # from its accepted upstream is invisible in every other check.
1242
+ errors.append(
1243
+ f"{shot_id}: 分镜时长 {shot_seconds:g} 秒与视频提示词 "
1244
+ f"{motion_seconds:g} 秒不一致;视频提示词只能原样照抄已接受的镜头时长"
1245
+ )
1148
1246
  image_value = fields.get("图片提示词项", "")
1149
1247
  if not image_value:
1150
1248
  errors.append(f"{shot_id}: 缺少图片提示词项字段")
@@ -1178,11 +1276,30 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1178
1276
  # shot without one would make the coverage check below vacuous.
1179
1277
  keyframe = _copyable_prompt(shot_body, heading=r"冻结关键帧提示词")
1180
1278
  if keyframe is None:
1181
- errors.append(f"{shot_id}: 缺少唯一且非空的冻结关键帧提示词")
1279
+ errors.append(
1280
+ f"{shot_id}: 缺少唯一且非空的冻结关键帧提示词"
1281
+ + _copyable_prompt_cause(shot_body, heading=r"冻结关键帧提示词")
1282
+ )
1182
1283
  elif basis.parsed:
1183
1284
  _check_named_coverage(
1184
1285
  keyframe, shot_id, "冻结关键帧提示词", basis, visual_entries, errors
1185
1286
  )
1287
+ if basis.parsed and source_value:
1288
+ # A shot's 来源 is its claim on the screenplay: whatever it quotes,
1289
+ # this shot is the one that films it. Quoting an action performed by
1290
+ # someone the frame never shows takes that action off everyone's
1291
+ # list — no other shot claims it, and nothing reports it missing, so
1292
+ # it simply never gets filmed. Same rule as the keyframe: in frame,
1293
+ # or declared 画外.
1294
+ _check_named_coverage(
1295
+ source_value,
1296
+ shot_id,
1297
+ "来源引文",
1298
+ basis,
1299
+ visual_entries,
1300
+ errors,
1301
+ by_entry_name=True,
1302
+ )
1186
1303
 
1187
1304
  motion = motion_by_shot.get(shot_id)
1188
1305
  if not motion:
@@ -48,6 +48,11 @@ CREATOR_DOCUMENTS = (
48
48
  "图片提示词.md",
49
49
  "视频提示词.md",
50
50
  )
51
+ # The cut list is written after the media exists, so an episode that never
52
+ # reached editing is complete without one. It travels with a handover when it is
53
+ # there -- without it the next person has to re-cut the film from scratch -- but
54
+ # it is never reported as a missing creator document.
55
+ POST_PRODUCTION_DOCUMENTS = ("剪辑单.md",)
51
56
  EXPORT_MEDIA_DIRECTORY = "制作成果"
52
57
  EXPORT_SCHEMA = "1.0"
53
58
  EPISODE_ID_RE = re.compile(r"EP(?:[0-9]{3}|[1-9][0-9]{3,})")
@@ -1948,6 +1953,12 @@ def _export_episode(
1948
1953
  continue
1949
1954
  digest = _export_regular_file(source, target / name)
1950
1955
  files.append({"path": name, "sha256": digest})
1956
+ for name in POST_PRODUCTION_DOCUMENTS:
1957
+ source = episode_directory / name
1958
+ if not source.exists():
1959
+ continue
1960
+ digest = _export_regular_file(source, target / name)
1961
+ files.append({"path": name, "sha256": digest})
1951
1962
  media_root = episode_directory / EXPORT_MEDIA_DIRECTORY
1952
1963
  if include_media and media_root.exists():
1953
1964
  media_details = os.lstat(media_root)
@@ -18,6 +18,24 @@
18
18
  把两者拆开,是为了换装、受伤、湿污和年龄变化时仍能复用人物身份,同时让图片提示
19
19
  词与分镜精确选择一个相容造型。
20
20
 
21
+
22
+ ## 画面代称要覆盖每一种正文语言
23
+
24
+ `画面代称` 是下游覆盖检查唯一用来匹配的字符串:冻结关键帧、图片提示词、视频提示词里出现了这个词,
25
+ 才算「这一镜点了这个资产的名」。匹配是逐字的,不做翻译。
26
+
27
+ 问题在于三份正文的语言不一定相同:图片提示词与冻结关键帧跟 `format/prompt_language`,
28
+ 视频提示词优先跟已接受的 `video_prompt_language`。而校验器只按后者的解析结果决定要不要强制
29
+ 「代称不能等于名字」——于是一个 zh + en 混配的项目很容易写成**只有英文代称**,
30
+ 然后中文正文里这个资产永远匹配不上。
31
+
32
+ **这种失败是静默的**:匹配不上不会报错,只会让覆盖检查对这条资产整个不生效,
33
+ 看起来一路绿灯。所以:
34
+
35
+ - 一个资产的 `画面代称` 要**把它在各份正文里实际会出现的写法都列上**(中文名与英文代称并列),
36
+ 不是二选一;
37
+ - 正文里确实不该靠名字识别时才写 `画面代称:无`,那是明确的弃权,不是省事。
38
+
21
39
  ## Character:最小稳定识别集
22
40
 
23
41
  从剧本与 creator reference 中选择足以区别此人的持久锚点:
@@ -40,7 +40,16 @@
40
40
 
41
41
  `锁面` 是**要被逐字带进提示词正文的那一小段字**,不是描述,也不是给人读的说明:
42
42
 
43
- - 用项目的提示词语言写(`short-drama.json#/format/prompt_language`,没有配置时是 `en`),
43
+ - 锁面必须**逐字出现在它管到的每一份可复制正文里**。校验器会在三处查同一个字符串:
44
+ 图片提示词正文、冻结关键帧提示词、视频提示词正文。
45
+ - 这三处的语言不一定相同:图片提示词与冻结关键帧跟 `short-drama.json#/format/prompt_language`,
46
+ 视频提示词优先跟已接受的 `production_profile.choices.video_prompt_language`。
47
+ **两者不同时(例如创作者说明 zh、H3 方言要求正文 en),一个字符串无法同时出现在三处**——
48
+ 这是已知冲突,不是可以靠选对语言绕开的。两条出路:
49
+ - 优先把锁面写成**跨语言同形**的事实:色值(`#2F3A2C`)、编号、型号、专名、数字状态。
50
+ 这类锁面在中英正文里都是同一串字符,三处都能原样命中;
51
+ - 确实需要自然语言描述时,把锁的 `镜头:` / `图片:` 范围收窄到语言一致的那些正文,
52
+ 另一侧的同一事实另立一条锁,不要用一条锁去跨语言。
44
53
  因为它要落进可复制正文,不是落进创作者说明;
45
54
  - 只保留最小可辨识名词短语,通常是「颜色 + 材质/形制 + 物体」,例如
46
55
  `pale blue chunky knit wool sweater`;
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: short-drama-edit
3
+ description: 把已经生产出来的逐镜素材按分镜装配成短剧成片:定每镜入出点、镜序、台词完整性、字幕、响度与交付规格,写成 `剧集/<EP>/剪辑单.md` 并渲染出成片。用户说“把这些镜头剪成片”“做成片/装配/套剪”“定入出点”“加字幕”“统一响度”“成片太长/太碎”“导出可继续精修的工程”时使用;不生成新素材,不改剧本、分镜或视频提示词的语义。
4
+ license: MIT
5
+ ---
6
+
7
+ # 短剧剪辑与成片
8
+
9
+ 把 `剧集/<EP>/制作成果/` 里的逐镜素材装配成一条成片,取舍写进 `剧集/<EP>/剪辑单.md`。
10
+ 每段使用二级标题 `## CUT-...`,各自绑定一个 `MOTION-...` 和一个真实媒体文件。
11
+
12
+ 本阶段是套件里唯一同时看得见**成片**和**创作意图**的位置。上游每一份文档描述的都是「这一镜
13
+ 应该是什么」;只有到这里,才第一次能回答「生成出来的这一段,哪一部分真的能用」。这两件事经常
14
+ 不一致,而处理这个不一致正是剪辑的工作,不是缺陷。
15
+
16
+ ## Quick Start
17
+
18
+ ```text
19
+ 用 $short-drama-edit 把 EP001 已生产的镜头剪成成片
20
+ ```
21
+
22
+ ## 入口
23
+
24
+ 进入条件是**素材已经存在且可读**:`剧集/<EP>/制作成果/` 里有本次要用的视频文件。
25
+ 提示词写完、job 确认过、adapter 返回成功,都不等于素材可用;本阶段只认当前能打开的文件字节。
26
+
27
+ 素材不全时不要等齐。已有素材能构成一段连续可看的内容就可以先剪,把缺口写进「未采用镜头」并
28
+ 说明它缺的是文件还是质量。剪一版看得见的东西,比拿着完整清单空等更能暴露上游问题。
29
+
30
+ `剧集/<EP>/分镜.md` 与 `视频提示词.md` 是镜序和职责的来源。剪辑可以**取舍**它们,不能**改写**
31
+ 它们:删一镜、缩一镜、换顺序都属于本阶段;改台词、改镜头职责、改起终状态要回上游发修订请求。
32
+
33
+ 渲染与测量需要 `ffmpeg` / `ffprobe` 在 PATH 上。没有时 `edit_tool.py` 直接失败并说明,
34
+ 不用别的手段近似,也不把「没测」写成「通过」。
35
+
36
+ ## 工作流
37
+
38
+ 1. **逐段看完整素材,先标可用带,再谈剪辑。** 每个文件从头到尾看一遍,写下它实际能用的时间
39
+ 区间,以及区间之外发生了什么(起势静止、结尾漂移、发白、动作回弹、多余的重复表演)。
40
+ 这一步的产出是事实,不是决定。做法见 [生成素材的可用带](references/generated-footage.md)。
41
+ 2. **有台词的镜先按发声定边界。** 台词完整性优先于画面取舍:先确定这句话从哪一帧开始出声、
42
+ 哪一帧收尾,再在这个约束内选画面入出点。切在字中间是结构缺陷,不是节奏选择。
43
+ 3. **在可用带内选叙事入出点。** 入点取动作已经在进行中的那一帧,出点取信息完成之后、缺陷
44
+ 开始之前。逐段写清入点理由和出点理由。做法见 [入出点与镜序](references/cut-craft.md)。
45
+ 4. **排镜序并检查相邻关系。** 相邻两段不同时具备同景别、同机位、同运动方向;重复承担同一
46
+ 信息的段落删掉一段,而不是两段各留一半。
47
+ 5. **对一遍相邻段的画面。** 逐镜生成的素材会漂移:同一个人、同一张桌子的两条,
48
+ 回来可能差一档曝光、半步白平衡。差得看得出来时,在那一段写一行 `画面`。
49
+ 做法见 [接镜与色彩一致性](references/generated-footage.md#接镜与色彩一致性)。
50
+ 6. **处理声音接缝与响度。** 逐镜生成的素材各自带着自己的环境声或音乐,直接拼接会在每个剪辑
51
+ 点留下音床硬切。做法见 [声音、字幕与音乐](references/sound-and-subtitles.md)。
52
+ 7. **写字幕。** 字幕的每一个字都取自 `剧本.md` 的台词原文,不取自转写结果;标点可按字幕惯例调整,
53
+ 字不能改。转写只用来定位时间。
54
+ 8. **写出 `剪辑单.md`,再渲染。** 先有文档后有成片:文档是可审查、可修订、可复现的那一份,
55
+ 成片是它的产物。
56
+ 9. **按可测项验收成片,再交付。** 见 [交付与验收](references/delivery-verify.md)。
57
+
58
+ ## 剪辑单要求
59
+
60
+ - 正文开头写交付规格:`成片目标时长`、`画幅与帧率`、`交付响度`、`字幕`。目标时长是意图,
61
+ 实际时长可以不等于它;差多少、为什么,写在末尾报告里,不靠删内容硬凑。
62
+ - 每段一个 `## CUT-<EP>-<序号> · <中文名称>`,块内逐行写:
63
+
64
+ - `- 来源:<MOTION-...> · <项目相对路径>`——`MOTION-...` 必须是 `视频提示词.md` 里真实存在
65
+ 的条目,路径必须是当前可读的文件。
66
+ - `- 入点:<秒>` / `- 出点:<秒>` / `- 时长:<秒>`——三者必须自洽,`出点 - 入点 == 时长`。
67
+ 全部用秒,保留两位小数,不写时间码,不写帧号。
68
+ - `- 取舍:入点=<理由>;出点=<理由>`——理由写画面上发生了什么,不写「节奏需要」。
69
+ 这一行是本阶段唯一的创作记录;它解释的是为什么丢掉了没被选中的那些帧。
70
+ - `- 声音:<本段声音怎么处理>`——保留原声、压低、静音、交叉淡入、接住音乐落点,逐段说清。
71
+ - `- 画面:<接镜校正>` 或 `- 画面:无`——只在这一段与相邻段明显对不上时写,
72
+ 格式是「亮度 <数>」「饱和 <数>」「色温 <数>」三项的任意组合,用 `;` 分隔。
73
+ 详见 [接镜与色彩一致性](references/generated-footage.md#接镜与色彩一致性)。
74
+ - `- 字幕:<逐字台词原文>` 或 `- 字幕:无`。本段有多句台词时改用编号写法,
75
+ 每句自带时间:`- 字幕 1:0.20-1.50 就是什么`、`- 字幕 2:2.30-4.30 就是少了点东西`……
76
+ 编号从 1 连续,时间不得重叠。**一镜不等于一句**——一次三个来回的对话是一个过肩镜头,
77
+ 只写一条字幕会让另外两句在成片上无声地消失。
78
+ - 素材里没有被采用的镜头,在正文开头写一行 `- 未采用镜头:<MOTION-...>(理由:……)`,
79
+ 多项用 `;` 连接。理由区分两类:**文件缺失**(还没生产 / 生产失败)和**质量不可用**
80
+ (生成结果不能用)。两者在文档里长得一样,但一个要回生产、一个要回上游改提示词。
81
+ - 同一个 `MOTION-...` 可以拆成多个 `CUT-...`(同一段素材的两处分别使用),也可以整段不用。
82
+ 不能凭空出现没有素材来源的 `CUT-...`。
83
+ - 一段素材被用两次时,两个 `CUT-...` 的区间不得重叠:同一批帧在成片里出现两次是重复镜头,
84
+ 不是复用。确需重复出现(闪回、强调)时写明它是有意重复,并说明第二次为什么仍然成立。
85
+
86
+ ## 边界
87
+
88
+ - **不生成新素材。** 需要补拍、重出、换参考图,回 `$short-drama-produce`;需要改提示词,
89
+ 回 `$short-drama-video-prompts` 或 `$short-drama-image-prompts`。
90
+ - **不改上游语义。** 剪辑单可以让某句台词不出现在成片里(整镜未采用),不能改这句台词是什么。
91
+ 发现剧本层面的问题就点名报告,交回 `$short-drama-write`。
92
+ - **不做解说旁白。** 短剧成片靠人物对白和画面推进,不加第三人称叙述音轨。需要解说式成片是
93
+ 另一种产品形态,不在本套件的目标里。
94
+ - **不把技术成功写成质量结论。** 渲染完成、时长对上、文件存在,都不是「这条片子能看」。
95
+ 成片质量的判断交给 `$short-drama-review`,或交给创作者本人。
96
+ - **不替创作者决定要不要再剪一版。** 报告当前成片的可测事实和仍然存在的取舍选项,让创作者选。
97
+
98
+ ## 命令
99
+
100
+ 命令由 [edit_tool.py](scripts/edit_tool.py) 提供:
101
+
102
+ ```text
103
+ python3 <本技能目录>/scripts/edit_tool.py check <剧集/EP001> --project-root <project>
104
+ python3 <本技能目录>/scripts/edit_tool.py render <剧集/EP001> --project-root <project> [--no-subtitles] [--subtitles ffmpeg|remotion] [--remotion-concurrency N]
105
+ python3 <本技能目录>/scripts/edit_tool.py verify <剧集/EP001> --project-root <project>
106
+ ```
107
+
108
+ `check` 只核对不渲染:素材是否比剪辑单新(重出之后字幕时间必须重测)、区间自洽、`MOTION-...` 解析、文件存在、区间不越过素材时长、
109
+ 同源区间不重叠、字幕文字能在 `剧本.md` 里找到。其中「区间不越过素材时长」需要 `ffprobe`,
110
+ 没有时这一项报「未测」并说明原因,不静默跳过。`render` 按剪辑单切段、拼接、烧字幕、统一响度,
111
+ 输出 `剧集/<EP>/制作成果/成片/`。`verify` 只测量已经渲染出来的成片并回报数字,不改文件、
112
+ 不给质量结论。
113
+
114
+ 字幕默认由 ffmpeg 烧录,零依赖,长片也是秒级。`--subtitles remotion` 换成一条排版可调的
115
+ 路线,代价是装一次 Node 依赖,而且叠层要**逐帧过无头浏览器**渲整部成片——慢且吃内存,
116
+ 并发默认压在 2(`--remotion-concurrency` 可调,调高前先量本机)。取舍见
117
+ [声音、字幕与音乐](references/sound-and-subtitles.md),装法见
118
+ [Remotion 字幕叠层](assets/remotion/README.md)。
119
+
120
+ `render` 会保留每段切好的中间文件。它们是下一轮微调的输入,也是「这一刀到底切在哪」的证据;
121
+ 不要在报告里用「重新渲染一次就好」代替给出这些文件。
122
+
123
+ ## 按需知识
124
+
125
+ 默认只读本 SKILL、当前分镜和要用的素材。遇到对应问题时只打开一份:
126
+
127
+ - 阶段边界与规则分级:[阶段契约](references/stage-contract.md)
128
+ - 可用带、生成缺陷与逐镜取舍:[生成素材的可用带](references/generated-footage.md)
129
+ - 入出点、镜序与节奏:[入出点与镜序](references/cut-craft.md)
130
+ - 声音接缝、字幕与音乐落点:[声音、字幕与音乐](references/sound-and-subtitles.md)
131
+ - 成片可测项与验收:[交付与验收](references/delivery-verify.md)
132
+ - 剪辑单成稿样例:[剪辑单样例](assets/剪辑单.md)
133
+ - 可选的 Remotion 字幕排版:[Remotion 字幕叠层](assets/remotion/README.md)
134
+
135
+ ## 修订
136
+
137
+ 保留未受影响的 `CUT-...` ID。改一段的入出点不触碰其它段;删一段时说明它承担的信息由哪一段
138
+ 接住,或明确说明这条信息在成片里不再出现。重新渲染前先更新剪辑单,不要让成片领先于文档。
139
+
140
+ ## 完成
141
+
142
+ 每个 `CUT-...` 都绑定真实素材与自洽区间,台词在成片里完整可听,字幕逐字等于剧本原文,
143
+ 剪辑点没有可听的音床硬切,成片响度落在交付目标上,未采用镜头都写明理由并区分了缺文件与缺质量,
144
+ 即完成。目标时长与实际时长的差、以及仍然值得再剪一版的地方,写在末尾报告里交给创作者判断。
145
+
146
+ ## 安装维护
147
+
148
+ 只有安装、升级或排障时运行 `python3 scripts/selftest.py`。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "短剧剪辑"
3
+ short_description: "把逐镜生成的素材按分镜装配成有节奏、台词完整、可交付的短剧成片"
4
+ default_prompt: "使用 $short-drama-edit 把 EP001 已生产的镜头素材剪成成片。"
@@ -0,0 +1,60 @@
1
+ # Remotion 字幕叠层
2
+
3
+ `$short-drama-edit` 的**可选**字幕渲染路线。默认路线是 ffmpeg + libass,零依赖;这一条排版
4
+ 更好,但要装 Node 依赖,而且慢得多、吃内存——先看下面的「代价」。
5
+
6
+ ## 为什么会有第二条路
7
+
8
+ ASS 的字号是相对 `PlayRes` 的单位,渲染时再缩放一次,这层间接每次都要重推一遍,推错了
9
+ 在数字上看不出来(见[声音、字幕与音乐](../../references/sound-and-subtitles.md))。这里字号、
10
+ 描边、行距、安全区、折行都是 CSS,写的是画面像素,改完在 `npx remotion studio` 当场能看。
11
+
12
+ ## 装一次
13
+
14
+ 工作区在**项目之外**(默认 `~/.cache/short-drama-edit/remotion`)。`edit_tool.py` 每次运行都会
15
+ 把本目录的源码同步过去,所以要改排版就改这里的 `src/`,不要改工作区里的副本——它会被覆盖。
16
+
17
+ ```bash
18
+ cd ~/.cache/short-drama-edit/remotion && npm install
19
+ ```
20
+
21
+ `node_modules` 不进项目也不进本仓库。
22
+
23
+ ## 用
24
+
25
+ ```bash
26
+ python3 <本技能目录>/scripts/edit_tool.py render <剧集/EP001> --project-root <project> --subtitles remotion
27
+ ```
28
+
29
+ 它渲染出一段**透明**的字幕层(VP8 + alpha),再由 ffmpeg 叠到未经改动的画面上。画面本身
30
+ 不经过浏览器重绘,所以不多一次画质损失。
31
+
32
+ ## 代价
33
+
34
+ 叠层**逐帧**渲,长度等于整部成片:一分钟竖屏片一千多帧 1080×1920,每帧一次无头浏览器
35
+ 截图,没字幕的帧照渲。透明还要求 PNG 抓帧加 VP8 编码,Remotion 的性能文档把这两样都列为
36
+ 慢环节。默认路线是一次滤镜的事,这条是分钟级。
37
+
38
+ 内存才是会出事的地方。Remotion 默认按 CPU 核数开浏览器实例,每个各持一整帧——10 核 8 GB
39
+ 的机器上内存打满,整机停止响应,本套件就这么卡死过一次。所以 `edit_tool.py` 固定传
40
+ `--concurrency`,默认 2。要提高先量本机,别凭核数拍:
41
+
42
+ ```bash
43
+ cd ~/.cache/short-drama-edit/remotion && npx remotion benchmark
44
+ ```
45
+
46
+ ## 许可证
47
+
48
+ Remotion 个人与小团队免费,超出规模需商业授权,条款以 [remotion.dev](https://www.remotion.dev/)
49
+ 为准。本套件是 MIT,不包含也不代理这项授权,也不会替你安装它——缺依赖时只报错并给出命令。
50
+ 默认路线不涉及这件事。
51
+
52
+ ## 文件
53
+
54
+ | 文件 | 作用 |
55
+ |---|---|
56
+ | `src/schema.ts` | 叠层的输入形状:cues、画幅、fps、字号与安全区比例 |
57
+ | `src/Subtitles.tsx` | 排版本身。所有尺寸都是画面高度的比例,同一组数值对 768×1344 和 1080×1920 都成立 |
58
+ | `src/Root.tsx` | 合成注册;画幅、帧率、时长由调用方通过 `--props` 传入 |
59
+ | `src/font.ts` | 渲染前等字体就绪,并验证字体族真的存在——装不上时报错,而不是悄悄换一款字体出片 |
60
+ | `remotion.config.ts` | 固定成带 alpha 的输出,叠层必须透明 |
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "short-drama-subtitles",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Remotion subtitle layer for short-drama-edit. Renders a transparent overlay from cues.json.",
6
+ "scripts": {
7
+ "render": "remotion render Subtitles"
8
+ },
9
+ "dependencies": {
10
+ "@remotion/cli": "4.0.418",
11
+ "react": "19.0.0",
12
+ "react-dom": "19.0.0",
13
+ "remotion": "4.0.418"
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ import { Config } from "@remotion/cli/config";
2
+
3
+ // The overlay is composited onto untouched picture, so it must carry alpha.
4
+ Config.setVideoImageFormat("png");
5
+ Config.setPixelFormat("yuva420p");
6
+ Config.setCodec("vp8");
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { Composition } from "remotion";
3
+ import { Subtitles } from "./Subtitles";
4
+ import { defaultProps, SubtitleProps } from "./schema";
5
+
6
+ export const RemotionRoot: React.FC = () => {
7
+ return (
8
+ <Composition
9
+ id="Subtitles"
10
+ component={Subtitles}
11
+ defaultProps={defaultProps}
12
+ // The caller passes the real frame, fps and length through --props; these
13
+ // stand in only when the composition is opened in the studio by hand.
14
+ durationInFrames={Math.max(1, Math.round(defaultProps.durationInSeconds * defaultProps.fps))}
15
+ fps={defaultProps.fps}
16
+ width={defaultProps.width}
17
+ height={defaultProps.height}
18
+ calculateMetadata={({ props }: { props: SubtitleProps }) => ({
19
+ durationInFrames: Math.max(1, Math.round(props.durationInSeconds * props.fps)),
20
+ fps: props.fps,
21
+ width: props.width,
22
+ height: props.height,
23
+ })}
24
+ />
25
+ );
26
+ };
@@ -0,0 +1,88 @@
1
+ import React, { useMemo } from "react";
2
+ import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
3
+ import { Cue, SubtitleProps } from "./schema";
4
+ import { assertFamilyResolves, waitForFonts } from "./font";
5
+
6
+ waitForFonts();
7
+
8
+ /**
9
+ * A transparent subtitle layer, composited onto untouched picture.
10
+ *
11
+ * Everything here is expressed as a fraction of frame height, so the same
12
+ * numbers hold for 768×1344 and 1080×1920. That is the whole reason this file
13
+ * exists in pixels rather than in an ASS style: the subtitle renderer's own
14
+ * `PlayRes` scaling was applied twice once already, and type three times too
15
+ * large shipped in a sample film.
16
+ */
17
+ export const Subtitles: React.FC<SubtitleProps> = ({
18
+ cues,
19
+ fontScale,
20
+ bottomScale,
21
+ fontFamily,
22
+ }) => {
23
+ const frame = useCurrentFrame();
24
+ const { fps, height, width } = useVideoConfig();
25
+ const now = frame / fps;
26
+ const active = cues.find((cue: Cue) => now >= cue.start && now < cue.end);
27
+
28
+ const fontSize = height * fontScale;
29
+ // A dark rim on every side keeps white type legible over a bright frame
30
+ // without a caption box, which is what vertical drama expects. It depends
31
+ // only on the frame size, so it is built once — and it stays above the early
32
+ // return, because a hook may not be skipped on the frames with no line.
33
+ const shadow = useMemo(() => {
34
+ const rim = Math.max(2, height * 0.0024);
35
+ return [
36
+ `0 0 ${rim * 3}px rgba(0,0,0,0.85)`,
37
+ `${rim}px ${rim}px 0 rgba(0,0,0,0.92)`,
38
+ `-${rim}px ${rim}px 0 rgba(0,0,0,0.92)`,
39
+ `${rim}px -${rim}px 0 rgba(0,0,0,0.92)`,
40
+ `-${rim}px -${rim}px 0 rgba(0,0,0,0.92)`,
41
+ ].join(", ");
42
+ }, [height]);
43
+
44
+ // Checked against a line about to be filmed, so a missing font stops the
45
+ // render on the first subtitle frame rather than after the whole pass.
46
+ assertFamilyResolves(fontFamily, active?.text ?? "");
47
+ if (!active) return null;
48
+
49
+ // A short lift on entry reads as the line arriving with the delivery. It is
50
+ // over well before the first syllable ends, so it never delays reading.
51
+ const age = now - active.start;
52
+ const lift = Math.min(1, age / 0.12);
53
+ const eased = 1 - Math.pow(1 - lift, 3);
54
+
55
+ return (
56
+ <AbsoluteFill
57
+ style={{
58
+ justifyContent: "flex-end",
59
+ alignItems: "center",
60
+ paddingBottom: height * bottomScale,
61
+ paddingLeft: width * 0.06,
62
+ paddingRight: width * 0.06,
63
+ }}
64
+ >
65
+ <div
66
+ style={{
67
+ fontFamily,
68
+ fontSize,
69
+ fontWeight: 700,
70
+ color: "#ffffff",
71
+ textAlign: "center",
72
+ lineHeight: 1.28,
73
+ letterSpacing: fontSize * 0.02,
74
+ textShadow: shadow,
75
+ opacity: eased,
76
+ transform: `translateY(${(1 - eased) * fontSize * 0.25}px)`,
77
+ // A long line wraps rather than running off the frame — the failure
78
+ // that made the first shipped version unreadable.
79
+ maxWidth: "100%",
80
+ wordBreak: "break-word",
81
+ whiteSpace: "pre-wrap",
82
+ }}
83
+ >
84
+ {active.text}
85
+ </div>
86
+ </AbsoluteFill>
87
+ );
88
+ };