@oh-story/dsh 0.1.11 → 0.1.13

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 (264) hide show
  1. package/README.md +17 -11
  2. package/lib/client.js +85 -35
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +41 -36
  5. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
  6. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
  7. package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
  8. package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
  9. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
  10. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
  11. package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
  12. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
  13. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
  14. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
  15. package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
  16. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
  17. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
  18. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
  19. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
  20. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  21. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
  22. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
  23. package/lib/index.js +16 -7
  24. package/lib/index.js.map +2 -2
  25. package/lib/novel-to-game/README.md +98 -44
  26. package/lib/novel-to-game/README_ZH.md +100 -46
  27. package/lib/novel-to-game/VERSION +1 -1
  28. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
  29. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
  30. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
  31. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
  32. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
  33. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
  34. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
  35. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
  36. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
  37. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
  38. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
  39. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
  40. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
  41. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
  42. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
  43. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
  44. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
  45. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
  46. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
  47. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
  57. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
  58. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
  59. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
  60. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
  61. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
  62. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
  63. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
  64. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
  65. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
  66. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
  91. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
  92. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
  93. package/lib/novel-to-game/manifest.json +303 -78
  94. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
  95. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
  96. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
  98. package/lib/types/client/drama-production-view.d.ts +11 -0
  99. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  100. package/lib/types/client/file-activity.d.ts +1 -1
  101. package/lib/types/client/file-activity.d.ts.map +1 -1
  102. package/lib/types/client/production-prompts.d.ts +7 -1
  103. package/lib/types/client/production-prompts.d.ts.map +1 -1
  104. package/lib/types/client/production-runtime.d.ts +7 -2
  105. package/lib/types/client/production-runtime.d.ts.map +1 -1
  106. package/lib/types/drama-adapters.d.ts.map +1 -1
  107. package/lib/types/skill-provider.d.ts.map +1 -1
  108. package/lib/types/video-project.d.ts.map +1 -1
  109. package/lib/video-recap/CHANGELOG.md +108 -1
  110. package/lib/video-recap/README.en.md +18 -14
  111. package/lib/video-recap/README.md +18 -14
  112. package/lib/video-recap/manifest.json +297 -321
  113. package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
  114. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
  115. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
  116. package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
  117. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
  118. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
  119. package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
  120. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
  121. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
  122. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
  123. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
  124. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
  125. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
  126. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
  127. package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
  128. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
  129. package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
  130. package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
  131. package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
  132. package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
  133. package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
  134. package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
  135. package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
  136. package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
  137. package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
  138. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
  139. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
  140. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
  141. package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
  142. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
  143. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
  144. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
  145. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
  146. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
  147. package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
  148. package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
  149. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
  150. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
  151. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
  152. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
  153. package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
  154. package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
  155. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
  156. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
  157. package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
  158. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
  159. package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
  160. package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
  161. package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
  162. package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
  163. package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
  164. package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
  165. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
  166. package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
  167. package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
  168. package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
  169. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
  170. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
  171. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
  172. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
  173. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
  174. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
  175. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
  176. package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
  177. package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
  178. package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
  179. package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
  180. package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
  181. package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
  182. package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
  183. package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
  184. package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
  185. package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
  186. package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
  187. package/lib/video-recap/skills/video-script/SKILL.md +9 -7
  188. package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
  189. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
  190. package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
  191. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
  192. package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
  193. package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
  194. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
  195. package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
  196. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
  197. package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
  198. package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
  199. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
  200. package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
  201. package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
  202. package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
  203. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
  204. package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
  205. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
  206. package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
  207. package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
  208. package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
  209. package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
  210. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
  211. package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
  212. package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
  213. package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
  214. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
  215. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
  216. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
  217. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
  218. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
  219. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
  220. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
  221. package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
  222. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
  223. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
  224. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
  225. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
  226. package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
  227. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
  228. package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
  229. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
  230. package/package.json +45 -45
  231. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  232. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  233. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  234. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
  235. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
  236. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
  237. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
  238. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
  239. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
  240. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
  241. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
  242. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
  243. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
  244. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
  245. package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
  246. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
  247. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
  248. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
  249. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
  250. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
  251. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
  252. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
  253. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
  254. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
  255. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
  256. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
  257. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
  258. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
  259. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
  260. package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
  262. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
  263. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
  264. /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
@@ -2,9 +2,9 @@
2
2
  "schemaVersion": 1,
3
3
  "upstream": {
4
4
  "repository": "https://github.com/zenstory-ai/drama-skills.git",
5
- "commit": "0afa4ea253cf4d1aa2d134863b15a592547cfb54"
5
+ "commit": "4e48ccbf0f77da757d7cacc6937b1cc59c124845"
6
6
  },
7
- "generatedAt": "2026-09-28T04:11:28.975Z",
7
+ "generatedAt": "2026-10-03T14:31:32.480Z",
8
8
  "skills": [
9
9
  "short-drama",
10
10
  "short-drama-assets",
@@ -126,8 +126,8 @@
126
126
  },
127
127
  {
128
128
  "path": "skills/short-drama-assets/references/voice-direction.md",
129
- "sha256": "231eb47a9c84917777250a4e4f7f35ecf2d8fef39c12e846ec4b932f3e89a5f1",
130
- "bytes": 9819
129
+ "sha256": "e2dd623693632f29c56e65bb5f40a4450a9c2808dbf7680dbc91bd043c02c4b2",
130
+ "bytes": 10004
131
131
  },
132
132
  {
133
133
  "path": "skills/short-drama-assets/scripts/asset_check.py",
@@ -321,8 +321,8 @@
321
321
  },
322
322
  {
323
323
  "path": "skills/short-drama-edit/SKILL.md",
324
- "sha256": "c6f62bb505d554b0cfd9789552deec08c2a96289eeb83a1c72cccb59b2f10d82",
325
- "bytes": 13176
324
+ "sha256": "3a8c8e52f6d648cff82aedc783ef7aee65594ecb089cfc8da62c44b1f3227bb0",
325
+ "bytes": 15232
326
326
  },
327
327
  {
328
328
  "path": "skills/short-drama-edit/agents/openai.yaml",
@@ -449,15 +449,20 @@
449
449
  "sha256": "d6edfc8a528f6079894109ec0c650fb411b7ee04f3b4b5462c71564db9304219",
450
450
  "bytes": 5839
451
451
  },
452
+ {
453
+ "path": "skills/short-drama-edit/references/still-cuts.md",
454
+ "sha256": "7afa3517e1c257b457a2bf83b3f8c2b93d3b5e84dcccb856c482c90de292d9a3",
455
+ "bytes": 5860
456
+ },
452
457
  {
453
458
  "path": "skills/short-drama-edit/scripts/edit_tool.py",
454
- "sha256": "f9381a8e5f6e5aeb9aeb52d1489a89238ffb8f33fe263b039f1599824289dea8",
455
- "bytes": 90571
459
+ "sha256": "b9b7d5b7f47c5e2cddc3b42fd8a77973b25b7111540a5a80cccd22825ad048ef",
460
+ "bytes": 130747
456
461
  },
457
462
  {
458
463
  "path": "skills/short-drama-edit/scripts/selftest.py",
459
- "sha256": "fbe753edf05540a0da929435d0c1a1fb2d9736d836fbc2de2a6bbeb8de17b94f",
460
- "bytes": 24051
464
+ "sha256": "308760a10f0031cace5a47bdf3f1107a51bdca1e0cce4cee71466ad887ae2825",
465
+ "bytes": 26399
461
466
  },
462
467
  {
463
468
  "path": "skills/short-drama-image-prompts/SKILL.md",
@@ -606,8 +611,8 @@
606
611
  },
607
612
  {
608
613
  "path": "skills/short-drama-produce/SKILL.md",
609
- "sha256": "90716253be3e28eacd98071465faa8313de2b6598b6d1bc8b64b4fc8616a350e",
610
- "bytes": 14398
614
+ "sha256": "25ba8c4294806abe34c9c62477d8dc8c9e1e86ab210ce54c354959904f9ff760",
615
+ "bytes": 15124
611
616
  },
612
617
  {
613
618
  "path": "skills/short-drama-produce/agents/openai.yaml",
@@ -616,8 +621,8 @@
616
621
  },
617
622
  {
618
623
  "path": "skills/short-drama-produce/references/adapter-contract.md",
619
- "sha256": "34f4e51c8b76aebed3b49e35c15cab0372d55084f89423deee646f4be13fee5e",
620
- "bytes": 11062
624
+ "sha256": "b0b43959521f88f2ffc18a8d17507b2033d37dddc4ed1876804cac4a334b2699",
625
+ "bytes": 12009
621
626
  },
622
627
  {
623
628
  "path": "skills/short-drama-produce/references/providers/gpt-image-2.md",
@@ -651,13 +656,13 @@
651
656
  },
652
657
  {
653
658
  "path": "skills/short-drama-produce/scripts/production_tool.py",
654
- "sha256": "560808219145a958723ff70841d8bac6280eed0ea4d55feb18646444f8661e3e",
655
- "bytes": 87712
659
+ "sha256": "1f9e8e1e4f460a68736bb142280563df8ccaa06d1d97bac6be5e9f83aa27e3d6",
660
+ "bytes": 93209
656
661
  },
657
662
  {
658
663
  "path": "skills/short-drama-produce/scripts/provider_adapters.py",
659
- "sha256": "b311b155947f22ecce3f39d783f214aad84291db7966090a64f466259f9f76bf",
660
- "bytes": 70329
664
+ "sha256": "9555f41da83e3666324dafedaf271a0205ed924bd956424c037e71419d7c067b",
665
+ "bytes": 70718
661
666
  },
662
667
  {
663
668
  "path": "skills/short-drama-produce/scripts/selftest.py",
@@ -756,8 +761,8 @@
756
761
  },
757
762
  {
758
763
  "path": "skills/short-drama-storyboard/SKILL.md",
759
- "sha256": "464adcbe2d3c130eff7d67854cdcdc6a4071e860d7f7d0a253612c25854dc86c",
760
- "bytes": 18759
764
+ "sha256": "a603d2254e91d6dc323f69fd137620fdfdc36b1ff738f3ef292ba9c3f4a8b4f5",
765
+ "bytes": 19365
761
766
  },
762
767
  {
763
768
  "path": "skills/short-drama-storyboard/agents/openai.yaml",
@@ -826,8 +831,8 @@
826
831
  },
827
832
  {
828
833
  "path": "skills/short-drama-storyboard/references/production-shot-grammar.md",
829
- "sha256": "d023c14aace36f40cc05527ef7d2151c6d4bfff940a45206b5a84a17e7e31f22",
830
- "bytes": 21285
834
+ "sha256": "dc3f1d1ee10d9783a493a1f02b0029ee145bcc6d42457eed97f3a3e195327bfe",
835
+ "bytes": 21788
831
836
  },
832
837
  {
833
838
  "path": "skills/short-drama-storyboard/references/review-and-fixtures.md",
@@ -871,8 +876,8 @@
871
876
  },
872
877
  {
873
878
  "path": "skills/short-drama-video-prompts/SKILL.md",
874
- "sha256": "766cbacdff8bd4575f63d771159c2ddeca9485e19050fa67a61b264492481a7d",
875
- "bytes": 16065
879
+ "sha256": "a542f15744384e86d63cf4e07431fdcbbe741df3c8b3fa3984885b31185dbdf7",
880
+ "bytes": 16930
876
881
  },
877
882
  {
878
883
  "path": "skills/short-drama-video-prompts/agents/openai.yaml",
@@ -946,8 +951,8 @@
946
951
  },
947
952
  {
948
953
  "path": "skills/short-drama-video-prompts/references/performance-action-timing.md",
949
- "sha256": "bc6b40dd2ddb1839ce35f8439bedbc51ddd2d512022009c31ecc379b24b67711",
950
- "bytes": 12718
954
+ "sha256": "24cbb164e5a7983c0146ff65907ed6c0ff15d12df830e03c9472fcc474dae8a9",
955
+ "bytes": 13916
951
956
  },
952
957
  {
953
958
  "path": "skills/short-drama-video-prompts/references/production-prompt-grammar.md",
@@ -966,23 +971,23 @@
966
971
  },
967
972
  {
968
973
  "path": "skills/short-drama-video-prompts/references/seedance-2.5.md",
969
- "sha256": "cb7276819941ab6dcc3159c605402d04ad97c3d1d6ff30ae8092c695ba400e52",
970
- "bytes": 8652
974
+ "sha256": "32f1b81fe4636fc020011aec0f29255a236b5363126b1fc0af3c451f23b27d88",
975
+ "bytes": 9922
971
976
  },
972
977
  {
973
978
  "path": "skills/short-drama-video-prompts/references/stage-contract.md",
974
- "sha256": "2af34243155489e4494a6a6a9e49ffe74cd5421f949a3828da26c7e98c946d92",
975
- "bytes": 11676
979
+ "sha256": "5d299f955e97cb9b5779a257372c3d8cc3b66a60bd181895dfd526dc12a428b6",
980
+ "bytes": 12263
976
981
  },
977
982
  {
978
983
  "path": "skills/short-drama-video-prompts/references/target-model-profile.md",
979
- "sha256": "d2f6f1b95a4a03b72a18bb4bdd8a55899b9ffcc4bbc3508a62687621ead5734f",
980
- "bytes": 10081
984
+ "sha256": "a78fcf795de91f248652acf55cfbceb4aedfe930da60a0a273d40f620fa11c58",
985
+ "bytes": 10582
981
986
  },
982
987
  {
983
988
  "path": "skills/short-drama-video-prompts/references/wan-3.0.md",
984
- "sha256": "7cecdf5e686056cdf67783600ae5d66aa54bf6f6805d7c1096166f206a55945a",
985
- "bytes": 6694
989
+ "sha256": "a21375b61496b0506e220bd3a52a3138142852d11ef793550f0e1d0f24271339",
990
+ "bytes": 7120
986
991
  },
987
992
  {
988
993
  "path": "skills/short-drama-video-prompts/scripts/container_check.py",
@@ -1211,8 +1216,8 @@
1211
1216
  },
1212
1217
  {
1213
1218
  "path": "skills/short-drama/scripts/creator_markdown_check.py",
1214
- "sha256": "21f0af79598d6161cec8c7bf720fe9acbe4d4de4319c9eb1499c8e5a77277e97",
1215
- "bytes": 61927
1219
+ "sha256": "c09f9f510f0c8f18be8cae1bd00efcb35cc132c6a04606b53d686fee7fa4c632",
1220
+ "bytes": 76402
1216
1221
  },
1217
1222
  {
1218
1223
  "path": "skills/short-drama/scripts/project_tool.py",
@@ -65,6 +65,58 @@ REF_PURPOSES = (
65
65
  "结束帧",
66
66
  "风格",
67
67
  )
68
+ # A character's voice travels on its own line, `- 参考音频:`, never inside
69
+ # 输入参考图: the picture slots number `@图片N`/`<Picture N>`/`图N` and the audio
70
+ # slots number `@音频N`/`<Audio N>`/`音频N`, each from 1, so mixing them in one
71
+ # field would renumber every picture after the first voice.
72
+ AUDIO_SUFFIXES = ("wav", "mp3", "m4a", "aac", "flac")
73
+ AUDIO_REF_RE = re.compile(
74
+ r"(REF-[A-Z0-9-]+)(顺序:([1-9]\d*))· "
75
+ r"([^;\n]+?\.(?:" + "|".join(AUDIO_SUFFIXES) + r"))《([^》\n]+)》"
76
+ r"(用途:([^;)\n]+);角色:([^;)\n]+);控制:([^;)]+);不得控制:([^)]+))",
77
+ re.IGNORECASE,
78
+ )
79
+ AUDIO_PURPOSE = "音色"
80
+ AUDIO_SYNTAX = (
81
+ "REF-<槽位>(顺序:<n>)· <音频路径>《<中文名>》"
82
+ "(用途:音色;角色:<人物名>;控制:<范围>;不得控制:<范围>)"
83
+ )
84
+ # An audio file or 用途:音色 in a picture field is a voice binding written in
85
+ # the wrong place; the generic "broken REF syntax" would not say where it goes.
86
+ AUDIO_IN_IMAGE_FIELD_RE = re.compile(
87
+ r"\.(?:" + "|".join(AUDIO_SUFFIXES) + r"|opus)《|用途:音色", re.IGNORECASE
88
+ )
89
+ # Where 《视觉设定.md》 records a character's accepted voice reference: one
90
+ # line under that 人物 entry, path first.
91
+ VOICE_RECORD_RE = re.compile(
92
+ r"^[ \t ]*[-*+][ \t ]*声音参考[::](.*)$", re.MULTILINE
93
+ )
94
+ # Same filename rule as the binding's locator (`[^;\n]+?`), so any path a
95
+ # 参考音频 slot may name can also be the recorded one -- spaces included.
96
+ VOICE_RECORD_PATH_RE = re.compile(
97
+ r"([^(;\n]+?\.(?:" + "|".join(AUDIO_SUFFIXES) + r"))(?=(|$)", re.IGNORECASE
98
+ )
99
+ # The screenplay's dialogue grammar, mirrored from the write skill's
100
+ # screenplay_index.py (DIALOGUE_RE, and TAG_RE + VOICE_TAG_BODY_RE for [VO] and
101
+ # [OS]). Each is matched against a whole paragraph, so a line that wraps onto
102
+ # the next physical line is still one line of dialogue.
103
+ SCREENPLAY_DIALOGUE_RE = re.compile(
104
+ r"^(?P<speaker>[^\s:():\[\]#]{1,40})"
105
+ r"(?:([^()\r\n]+))?:(?P<text>\S[\s\S]*)$"
106
+ )
107
+ SCREENPLAY_VOICE_TAG_RE = re.compile(
108
+ r"^\[(?:VO|OS)\]\s*(?P<speaker>[^\s:():\[\]#]{1,40}):(?P<text>\S[\s\S]*)$"
109
+ )
110
+ # A later line of a paragraph that starts another block (the index's
111
+ # ASCII_DIALOGUE_RE, ANY_TAG_RE and MALFORMED_TAG_RE): the paragraph is missing
112
+ # a separator and the index emits no dialogue for it.
113
+ SCREENPLAY_BLOCK_START_RE = re.compile(
114
+ r"^[^\s:():\[\]#]{1,40}(?:([^()\r\n]+))?:\s*\S"
115
+ r"|^\[[^\]\r\n]+\]"
116
+ r"|^\[(?:VO|OS|SFX|画面文字|连续性|转场)(?:\s|:|:)"
117
+ )
118
+ # Where the clause that introduces a quote begins.
119
+ CLAUSE_BREAK_RE = re.compile(r"[。!?;.!?;]")
68
120
  # `EP001-SC001` is the documented shape, but a project that scopes ids by season
69
121
  # writes `S01-EP001-SC001`. Both are one stable scene id, so the pattern takes
70
122
  # any hyphenated prefix rather than exactly one segment.
@@ -334,6 +386,12 @@ def _references(
334
386
  """Validate one 输入参考图/参考 declaration; return the slot kinds it uses."""
335
387
  if _is_none(value):
336
388
  return frozenset()
389
+ if AUDIO_IN_IMAGE_FIELD_RE.search(value):
390
+ errors.append(
391
+ f"{owner}: 输入参考图只收图片;角色音色参考写在《视频提示词.md》"
392
+ "该 MOTION 的「- 参考音频:」行"
393
+ )
394
+ return frozenset()
337
395
  reference_value = PENDING_REFERENCE_SUFFIX_RE.sub("", value.strip())
338
396
  matches = _slot_matches(reference_value)
339
397
  cursor = 0
@@ -405,16 +463,7 @@ def _references(
405
463
  slot, raw_locator, label = group[0], group[2], group[3]
406
464
  may_control, must_not_control = group[5], group[6]
407
465
  if kind == "REF":
408
- if not _portable_path(raw_locator):
409
- errors.append(
410
- f"{owner}: REF 路径不是安全的项目相对路径: {raw_locator}"
411
- )
412
- else:
413
- reference_path = project_root / raw_locator
414
- if not _inside(reference_path, project_root):
415
- errors.append(f"{owner}: REF 路径越出项目根目录: {raw_locator}")
416
- elif not reference_path.is_file():
417
- errors.append(f"{owner}: REF 文件不存在: {raw_locator}")
466
+ _check_project_file(raw_locator, owner, project_root, errors)
418
467
  else:
419
468
  _plan_locator(
420
469
  raw_locator, label, owner, slot, purposes[index], entries, errors
@@ -430,6 +479,253 @@ def _references(
430
479
  return kinds
431
480
 
432
481
 
482
+ def _check_project_file(
483
+ locator: str, owner: str, project_root: Path, errors: list[str]
484
+ ) -> None:
485
+ """A REF locator names a readable file inside the project, or it is a defect."""
486
+ if not _portable_path(locator):
487
+ errors.append(f"{owner}: REF 路径不是安全的项目相对路径: {locator}")
488
+ return
489
+ reference_path = project_root / locator
490
+ if not _inside(reference_path, project_root):
491
+ errors.append(f"{owner}: REF 路径越出项目根目录: {locator}")
492
+ elif not reference_path.is_file():
493
+ errors.append(f"{owner}: REF 文件不存在: {locator}")
494
+
495
+
496
+ def _han_key(value: str) -> str:
497
+ """Only the Han characters, so quoting and language tags do not decide identity."""
498
+ return "".join(re.findall(r"[\u3400-\u9fff]+", value))
499
+
500
+
501
+ class SpokenLine(NamedTuple):
502
+ """One dialogue paragraph of 剧本.md: its scene, speaker and Han text."""
503
+
504
+ scene: Optional[str]
505
+ speaker: str
506
+ key: str
507
+
508
+
509
+ def _screenplay_dialogue(screenplay: str) -> list[SpokenLine]:
510
+ """Every dialogue paragraph, read the way screenplay_index.py reads it.
511
+
512
+ Mirrors `_parse_screenplay` block by block: a comment runs through the line
513
+ that closes it, and an unclosed one ends the parse; a heading sets or clears
514
+ the scene (scene ids use this checker's own SCENE_HEADING_RE, the one
515
+ 来源 resolves against); a paragraph runs until a blank line, a heading or a comment and
516
+ counts only inside a scene; a later line that starts another block means a
517
+ missing separator, so the paragraph is not dialogue. The one difference is
518
+ the speaker list: the index takes it from the project, and here a speaker
519
+ is only ever looked up by 人物 entry name.
520
+ """
521
+ found: list[SpokenLine] = []
522
+ scene: Optional[str] = None
523
+ lines = screenplay.splitlines()
524
+ index = 0
525
+ while index < len(lines):
526
+ stripped = lines[index].strip()
527
+ if not stripped:
528
+ index += 1
529
+ continue
530
+ if stripped.startswith("<!--"):
531
+ end = index
532
+ while end < len(lines) and "-->" not in lines[end]:
533
+ end += 1
534
+ if end >= len(lines):
535
+ break
536
+ index = end + 1
537
+ continue
538
+ if stripped.startswith("#"):
539
+ heading = SCENE_HEADING_RE.match(stripped)
540
+ scene = heading.group(1) if heading else None
541
+ index += 1
542
+ continue
543
+ end = index
544
+ while end + 1 < len(lines):
545
+ following = lines[end + 1].strip()
546
+ if not following or following.startswith(("#", "<!--")):
547
+ break
548
+ end += 1
549
+ paragraph = [line.strip() for line in lines[index : end + 1]]
550
+ index = end + 1
551
+ if scene is None or any(
552
+ SCREENPLAY_DIALOGUE_RE.fullmatch(line) or SCREENPLAY_BLOCK_START_RE.match(line)
553
+ for line in paragraph[1:]
554
+ ):
555
+ continue
556
+ text = "\n".join(paragraph)
557
+ match = SCREENPLAY_VOICE_TAG_RE.fullmatch(text) or SCREENPLAY_DIALOGUE_RE.fullmatch(
558
+ text
559
+ )
560
+ if match is not None and _han_key(match.group("text")):
561
+ found.append(
562
+ SpokenLine(scene, match.group("speaker"), _han_key(match.group("text")))
563
+ )
564
+ return found
565
+
566
+
567
+ def _quote_speakers(
568
+ prompt: Optional[str], lines: list[SpokenLine], names: dict[str, list[str]]
569
+ ) -> list[tuple[str, frozenset]]:
570
+ """Each quote in the copyable body with the speakers it can belong to.
571
+
572
+ Dialogue reaches the body quoted and verbatim (VID-25), so the screenplay
573
+ says whose line it is. A short quote such as 「是。」 has to match a whole
574
+ line, or any one-character quote would count as anybody's. When two
575
+ speakers share the line, the clause that introduces the quote decides it
576
+ only if it names exactly one of them; otherwise the quote stays ambiguous.
577
+ """
578
+ if prompt is None:
579
+ return []
580
+ joined = prompt.replace("\n", "")
581
+ result: list[tuple[str, frozenset]] = []
582
+ previous_end = 0
583
+ for match in SPOKEN_SPAN_RE.finditer(joined):
584
+ span = next((group for group in match.groups() if group), "")
585
+ key = _han_key(span) if len(span) <= 200 else ""
586
+ candidates = frozenset(
587
+ line.speaker
588
+ for line in lines
589
+ if key and (key == line.key or (len(key) >= 4 and key in line.key))
590
+ )
591
+ if len(candidates) > 1:
592
+ lead = joined[previous_end : match.start()]
593
+ breaks = list(CLAUSE_BREAK_RE.finditer(lead))
594
+ clause = lead[breaks[-1].end() :] if breaks else lead
595
+ named = frozenset(
596
+ speaker
597
+ for speaker in candidates
598
+ if any(name in clause for name in names.get(speaker, [speaker]))
599
+ )
600
+ if len(named) == 1:
601
+ candidates = named
602
+ if candidates:
603
+ result.append((span, candidates))
604
+ previous_end = match.end()
605
+ return result
606
+
607
+
608
+ def _voice_records(visual: str, errors: list[str]) -> dict[str, str]:
609
+ """Each 人物 entry's `- 声音参考:` value, keyed by the entry name."""
610
+ headings = list(re.finditer(r"^## [^\n]*$", visual, re.MULTILINE))
611
+ records: dict[str, str] = {}
612
+ for index, heading in enumerate(headings):
613
+ entry = VISUAL_SETTING_HEADING_RE.match(heading.group(0))
614
+ if entry is None or entry.group(1) != "人物":
615
+ continue
616
+ end = headings[index + 1].start() if index + 1 < len(headings) else None
617
+ declared = VOICE_RECORD_RE.findall(visual[heading.end() : end])
618
+ if len(declared) > 1:
619
+ errors.append(f"视觉设定.md: 人物「{entry.group(2).strip()}」的声音参考重复,只能登记一条")
620
+ elif declared:
621
+ records[entry.group(2).strip()] = _plain(declared[0])
622
+ return records
623
+
624
+
625
+ class VoiceContext(NamedTuple):
626
+ """What a MOTION's 参考音频 line is checked against."""
627
+
628
+ characters: frozenset
629
+ records: dict[str, str]
630
+ dialogue: list[SpokenLine]
631
+ # Every name a clause may use for a 人物 entry: its name and 画面代称.
632
+ names: dict[str, list[str]]
633
+
634
+
635
+ def _audio_references(
636
+ value: str,
637
+ owner: str,
638
+ project_root: Path,
639
+ errors: list[str],
640
+ *,
641
+ image_slots: list[str],
642
+ prompt: Optional[str],
643
+ voices: VoiceContext,
644
+ scenes: list[str],
645
+ ) -> None:
646
+ """Validate one MOTION's 参考音频 line against the character it names."""
647
+ if _is_none(value):
648
+ return
649
+ # A line said in another scene is not evidence for this shot.
650
+ in_scope = [line for line in voices.dialogue if not scenes or line.scene in scenes]
651
+ quotes = _quote_speakers(prompt, in_scope, voices.names)
652
+ plain = value.strip()
653
+ matches = list(AUDIO_REF_RE.finditer(plain))
654
+ cursor = 0
655
+ for index, match in enumerate(matches):
656
+ if plain[cursor : match.start()] != ("" if index == 0 else ";"):
657
+ matches = []
658
+ break
659
+ cursor = match.end()
660
+ if not matches or plain[cursor:] not in {"", "。"}:
661
+ errors.append(f"{owner}: 参考音频必须写成 {AUDIO_SYNTAX},多条用;连接")
662
+ return
663
+ slots = [match.group(1) for match in matches]
664
+ orders = sorted(int(match.group(2)) for match in matches)
665
+ if len(set(slots)) != len(slots) or set(slots) & set(image_slots):
666
+ errors.append(f"{owner}: 参考音频槽位重复,或与输入参考图的槽位重名")
667
+ if orders != list(range(1, len(orders) + 1)):
668
+ errors.append(f"{owner}: 参考音频顺序必须唯一且从 1 连续编号,与图片分开数")
669
+ speakers = [match.group(6).strip() for match in matches]
670
+ for name in sorted({name for name in speakers if speakers.count(name) > 1}):
671
+ errors.append(f"{owner}: 人物「{name}」绑定了不止一条参考音频")
672
+ for match in matches:
673
+ slot, _, locator, label, purpose, _, may, must = match.groups()
674
+ name = match.group(6).strip()
675
+ _check_project_file(locator, owner, project_root, errors)
676
+ if purpose.strip() != AUDIO_PURPOSE:
677
+ errors.append(
678
+ f"{owner}: 参考音频用途只能是{AUDIO_PURPOSE}: {slot}({purpose.strip()})"
679
+ )
680
+ if not re.search(r"[\u4e00-\u9fff]", label):
681
+ errors.append(f"{owner}: REF 缺少中文名称: {slot}")
682
+ allowed = {item.strip() for item in re.split(r"[、,,]", may)}
683
+ prohibited = {item.strip() for item in re.split(r"[、,,]", must)}
684
+ if "" in allowed or "" in prohibited or allowed & prohibited:
685
+ errors.append(f"{owner}: REF 控制与不得控制范围冲突: {slot}")
686
+ if not {"台词", "语气", "情绪"} <= prohibited:
687
+ errors.append(f"{owner}: 参考音频不得控制至少包含台词、语气、情绪: {slot}")
688
+ if name not in voices.characters:
689
+ errors.append(
690
+ f"{owner}: 参考音频的角色不是《视觉设定.md》里的人物条目: {name}"
691
+ )
692
+ continue
693
+ shared = [span for span, speakers in quotes if name in speakers]
694
+ if any(speakers == {name} for _, speakers in quotes):
695
+ pass
696
+ elif shared:
697
+ others = sorted(
698
+ {s for _, speakers in quotes if name in speakers for s in speakers} - {name}
699
+ )
700
+ errors.append(
701
+ f"{owner}: 引文「{_excerpt(shared[0], 24)}」在《剧本.md》里也是"
702
+ f"{'、'.join(others)}的台词,看不出是不是人物「{name}」在说;"
703
+ "在引文前的分句里点名说话人,或引一句只属于他的台词"
704
+ )
705
+ else:
706
+ errors.append(
707
+ f"{owner}: 人物「{name}」在本镜可复制提示词里没有说出《剧本.md》记在其名下的台词;"
708
+ "不说话的人物不绑定参考音频"
709
+ )
710
+ record = voices.records.get(name)
711
+ recorded = VOICE_RECORD_PATH_RE.match(record.strip()) if record else None
712
+ if record is None:
713
+ errors.append(
714
+ f"{owner}: 《视觉设定.md》人物「{name}」没有声音参考行;"
715
+ "先在该人物条目下写「- 声音参考:<项目相对路径>(…)」"
716
+ )
717
+ elif recorded is None:
718
+ errors.append(
719
+ f"{owner}: 《视觉设定.md》人物「{name}」的声音参考还没有音频路径: "
720
+ f"{_excerpt(record, 24)}"
721
+ )
722
+ elif recorded.group(1) != locator:
723
+ errors.append(
724
+ f"{owner}: 参考音频 {slot} 的路径 {locator} 与《视觉设定.md》"
725
+ f"人物「{name}」登记的声音参考 {recorded.group(1)} 不一致"
726
+ )
727
+
728
+
433
729
  def _plan_locator(
434
730
  locator: str,
435
731
  label: str,
@@ -1121,6 +1417,18 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1121
1417
  claimed_scenes: set = set()
1122
1418
  locks = _continuity_locks(visual, errors)
1123
1419
  visual_entries = _visual_entries(visual, errors)
1420
+ voices = VoiceContext(
1421
+ characters=frozenset(
1422
+ entry.name for entry in visual_entries if entry.category == "人物"
1423
+ ),
1424
+ records=_voice_records(visual, errors),
1425
+ dialogue=_screenplay_dialogue(screenplay),
1426
+ names={
1427
+ entry.name: _unique([entry.name, *entry.designators])
1428
+ for entry in visual_entries
1429
+ if entry.category == "人物"
1430
+ },
1431
+ )
1124
1432
  other_headings = {
1125
1433
  match.group(2).strip(): match.group(1).strip()
1126
1434
  for match in OTHER_SETTING_HEADING_RE.finditer(visual)
@@ -1226,9 +1534,8 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1226
1534
  for shot_id, shot_body in shots.items():
1227
1535
  fields = _fields(shot_body, owner=shot_id, errors=errors)
1228
1536
  source_value = fields.get("来源", "")
1229
- claimed_scenes.update(
1230
- _shot_sources(source_value, shot_id, scenes, errors)
1231
- )
1537
+ shot_scenes = _shot_sources(source_value, shot_id, scenes, errors)
1538
+ claimed_scenes.update(shot_scenes)
1232
1539
  shot_seconds = _declared_seconds(_plain(fields.get("时长", "")))
1233
1540
  motion_seconds = _declared_seconds(motion_duration.get(shot_id, ""))
1234
1541
  if (
@@ -1262,6 +1569,10 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1262
1569
  shot_input = fields.get("输入参考图", "")
1263
1570
  if not shot_input:
1264
1571
  errors.append(f"{shot_id}: 缺少输入参考图字段")
1572
+ if "参考音频" in fields:
1573
+ errors.append(
1574
+ f"{shot_id}: 参考音频写在《视频提示词.md》的 MOTION 条目里,分镜不登记"
1575
+ )
1265
1576
  _references(shot_input, shot_id, project_root, errors, entries)
1266
1577
 
1267
1578
  basis_value = fields.get("视觉依据", "")
@@ -1328,6 +1639,22 @@ def validate_episode(episode: Path, project_root: Optional[Path] = None) -> list
1328
1639
  _references(motion_input, motion_id, project_root, errors, entries)
1329
1640
  if _plain(motion_input) != _plain(shot_input):
1330
1641
  errors.append(f"{motion_id}: 输入参考图与 {shot_id} 不一致")
1642
+ if "参考音频" in motion_fields:
1643
+ _audio_references(
1644
+ motion_fields["参考音频"],
1645
+ motion_id,
1646
+ project_root,
1647
+ errors,
1648
+ image_slots=[
1649
+ item[3][0]
1650
+ for item in _slot_matches(
1651
+ PENDING_REFERENCE_SUFFIX_RE.sub("", motion_input.strip())
1652
+ )
1653
+ ],
1654
+ prompt=copyable_prompt,
1655
+ voices=voices,
1656
+ scenes=shot_scenes,
1657
+ )
1331
1658
 
1332
1659
  if _has_pending_references(shot_input):
1333
1660
  errors.append(f"{motion_id}: 仍有待补参考图,不能生成最终视频提示词")
@@ -165,7 +165,8 @@ CHAR-A 与 CHAR-B 最易混:两条参考同为中低音区、语速偏慢。
165
165
 
166
166
  需要跨集保持声音身份时,在对应人物条目下增加“声音方向”,只保留:
167
167
 
168
- 1. **参考**:项目相对路径、用途、允许借用与不得借用;没有参考时写“待选型”;
168
+ 1. **参考**:一行 `- 声音参考:<项目相对路径>(用途:…;控制:…;不得控制:…;状态:<admission_status>)`,
169
+ 路径打头;没有参考时写 `- 声音参考:待选型`;
169
170
  2. **选型判据**:三到五条可听判据,每条带反例;
170
171
  3. **对比角色**:最容易混淆的是谁,以及靠什么可听差异区分;
171
172
  4. **专名发音**:唯一确认读法;
@@ -174,6 +175,7 @@ CHAR-A 与 CHAR-B 最易混:两条参考同为中低音区、语速偏慢。
174
175
  临时情绪、呼吸、重音、音量和空间混响仍由剧本/镜头/视频提示词拥有。声音方向是生产输入,不是
175
176
  合成结果保证;实际 TTS 仍由 `$short-drama-produce` 在精确预览并显式确认后执行。
176
177
 
177
- 参考音频落地之后,它在下游有两个去处:作为视频模型的音色参考绑定(见目标模型的方言文档),
178
+ 参考音频落地之后,它在下游有两个去处:作为视频模型的音色参考绑定(《视频提示词.md》该镜的
179
+ `参考音频` 行照抄这条路径,由 `$short-drama-video-prompts` 写),
178
180
  以及作为配音本的实际人声。配音使用同一已选音色逐句合成;参考音频用于选型或模型绑定,不能直接充当不同台词的成品音轨。
179
181
  是否保持角色声音,仍需在生成后回听确认。