@oh-story/dsh 0.1.12 → 0.1.14

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 (269) hide show
  1. package/README.md +18 -12
  2. package/lib/client.js +1003 -652
  3. package/lib/client.js.map +4 -4
  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 +17 -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/canvas-viewport.d.ts +31 -0
  99. package/lib/types/client/canvas-viewport.d.ts.map +1 -0
  100. package/lib/types/client/drama-production-view.d.ts +14 -2
  101. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  102. package/lib/types/client/file-activity.d.ts +1 -1
  103. package/lib/types/client/file-activity.d.ts.map +1 -1
  104. package/lib/types/client/index.d.ts.map +1 -1
  105. package/lib/types/client/production-canvas.d.ts +24 -0
  106. package/lib/types/client/production-canvas.d.ts.map +1 -0
  107. package/lib/types/client/production-prompts.d.ts +7 -1
  108. package/lib/types/client/production-prompts.d.ts.map +1 -1
  109. package/lib/types/client/production-runtime.d.ts +7 -2
  110. package/lib/types/client/production-runtime.d.ts.map +1 -1
  111. package/lib/types/drama-adapters.d.ts.map +1 -1
  112. package/lib/types/skill-provider.d.ts.map +1 -1
  113. package/lib/types/video-project.d.ts.map +1 -1
  114. package/lib/video-recap/CHANGELOG.md +123 -1
  115. package/lib/video-recap/README.en.md +20 -16
  116. package/lib/video-recap/README.md +20 -16
  117. package/lib/video-recap/manifest.json +297 -321
  118. package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
  119. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
  120. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
  121. package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
  122. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
  123. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
  124. package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
  125. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
  126. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
  127. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
  128. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
  129. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
  130. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
  131. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
  132. package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
  133. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
  134. package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
  135. package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
  136. package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
  137. package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
  138. package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
  139. package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
  140. package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
  141. package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
  142. package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
  143. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
  144. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
  145. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
  146. package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
  147. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
  148. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
  149. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
  150. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
  151. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
  152. package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
  153. package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
  154. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
  155. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
  156. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
  157. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
  158. package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
  159. package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
  160. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
  161. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
  162. package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
  163. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
  164. package/lib/video-recap/skills/video-recap/SKILL.md +28 -29
  165. package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
  166. package/lib/video-recap/skills/video-recap/references/config-playbook.md +10 -13
  167. package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
  168. package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
  169. package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
  170. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
  171. package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
  172. package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
  173. package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
  174. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +55 -17
  175. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
  176. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +96 -117
  177. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
  178. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
  179. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
  180. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
  181. package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
  182. package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
  183. package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
  184. package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
  185. package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
  186. package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
  187. package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
  188. package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
  189. package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
  190. package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
  191. package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
  192. package/lib/video-recap/skills/video-script/SKILL.md +53 -24
  193. package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
  194. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
  195. package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
  196. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
  197. package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
  198. package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
  199. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
  200. package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
  201. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
  202. package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
  203. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -11
  204. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
  205. package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
  206. package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
  207. package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
  208. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
  209. package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
  210. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
  211. package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
  212. package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
  213. package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
  214. package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
  215. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
  216. package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
  217. package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
  218. package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
  219. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
  220. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
  221. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
  222. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
  223. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
  224. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
  225. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
  226. package/lib/video-recap/skills/video-voiceover/SKILL.md +51 -18
  227. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
  228. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
  229. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +81 -135
  230. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
  231. package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
  232. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
  233. package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
  234. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
  235. package/package.json +45 -45
  236. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  237. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  238. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  239. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
  240. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
  241. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
  242. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
  243. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
  244. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
  245. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
  246. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
  247. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
  248. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
  249. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
  250. package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
  251. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
  252. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
  253. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
  254. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
  255. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
  256. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
  257. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
  258. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
  259. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
  260. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
  261. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
  262. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
  263. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
  264. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
  265. package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
  266. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
  267. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
  268. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
  269. /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
@@ -5,6 +5,9 @@ description: >
5
5
  宣发标题、花字修订和外部文案回填。普通策划输入 work_dir 的 agent_narration_brief.md 与
6
6
  vlm_analysis.json;文案返修输入当前成片的工程与内容证据。策划输出 recap_story_plan.json、visual_audio_board.json、
7
7
  可选 style_card.json、cut 模式需要的 clip_plan.json,以及通过校验的 narration.json;仅宣发文案任务交付提案或回填既有包装计划。
8
+ 外发说明:只有建议型评审 review.py 联网,它把旁白稿全文与理解证据、策划文件的文字摘录发到 MiMo chat 接口
9
+ (MIMO_API_KEY / MIMO_API_URL,不发视频、图片或音频);单独使用时只在显式执行时运行,端到端编排默认在 TTS 前运行一次,
10
+ 可用 --no-review-narration / REVIEW_NARRATION=0 关闭(严格评审开启时除外);validate.py 与 lint 仅在本地运行。
8
11
  触发词:解说词、写解说、视频旁白、宣发标题、花字修订、文案回填、
9
12
  narration script、写稿、解说文案、剪辑思路、导演思路。
10
13
  ---
@@ -23,7 +26,7 @@ description: >
23
26
  4. 声音/旁白编辑
24
27
  5. 第一次观看的观众
25
28
 
26
- Agent 先记录简洁决定,再写时间线产物。`validate.py` 负责对理解索引做机械校验;full 模式默认还会执行预算整理并计算旁白的原声重叠。已有批准稿应加 `--preserve-approved-text`,保留段落顺序、数量、时间、文本、停顿和扩展元数据,仅允许依据现有声音证据更新 `overlaps_speech`。
29
+ Agent 先记录简洁决定,再写时间线产物。`validate.py` 负责对理解索引做机械校验,从不改写 Agent 的稿子:段落顺序、数量、时间、文本、停顿和扩展元数据原样保留,只依据现有声音证据回写实测的 `overlaps_speech`。文本装不下时间窗、段落未按时间排序等问题以 error 退回给 Agent 修改。
27
30
 
28
31
  下面的 `scripts/...` 均相对于本技能目录。若执行器从仓库根目录启动,请给脚本路径加上本技能的绝对目录。
29
32
 
@@ -37,15 +40,39 @@ Agent 先记录简洁决定,再写时间线产物。`validate.py` 负责对理
37
40
 
38
41
  REVISION 先明确本轮修改项与冻结项,再编辑对应层:表达、口语节奏、字幕反馈更新 `style_card.json`;镜头、入出点、表演和声音分工更新 `visual_audio_board.json`;只有观众承诺、POV、主线或 beat 改变时才更新 `recap_story_plan.json`。被删除的镜头、原声或文案也要从相关计划中删除,不能保留过期锚点。不要把看片修改重新做成一次 CREATE。
39
42
 
40
- ## 2. 读取素材并确认状态
43
+ ## 2. 远程调用与数据外发
44
+
45
+ 本技能只有一个脚本会联网:`review.py`(§7.1 的建议型语义评审)。其余全部在本地运行:`validate.py` 及其 lint、去 AI 味检查只读写 `work_dir` 里的文件,不发任何网络请求;策划与写稿由 Agent 直接读写 `work_dir` 完成。
46
+
47
+ `review.py` 发给谁:
48
+
49
+ - MiMo 的 OpenAI 兼容 chat 接口 `<MIMO_API_URL>/chat/completions`,用 `MIMO_API_KEY` 认证,模型为 `MIMO_MODEL`(默认 `mimo-v2.5`)。未设 `MIMO_API_URL` 时,按量计费 key 发往 `https://api.xiaomimimo.com/v1`,`tp-` 开头的 Token Plan key 发往 `MIMO_TOKEN_PLAN_CLUSTER` 选定的 token-plan 集群(默认 `https://token-plan-cn.xiaomimimo.com/v1`)。
50
+ - 未设 `MIMO_API_KEY` 时直接退出,不构造也不发送任何请求。
51
+
52
+ `review.py` 发送什么(只有文字,不发视频、帧图片或音频文件):
53
+
54
+ - `narration.json` 全部旁白段的时间与文字。
55
+ - 理解索引的文字证据:开头、中段、结尾、长空档与各旁白时间窗附近选出的时间段内,`vlm_analysis.json` 的场景描述与 `frame_facts` 文字,以及对白转写(有 `asr_clean.json` 时用它,否则 `asr_result.json`)。
56
+ - `background_research.json` 的梗概、角色、关系与剧情线摘录。
57
+ - 存在时附上 `packaging_plan.json`、`recap_story_plan.json`、`visual_audio_board.json`、`style_card.json`(各截前 3000 字)和 `original_subtitles.json` 的原声字幕文字。
58
+ - 内容较长时分块,每块一次请求;返回的意见只写回本地 `narration_review.json` / `narration_review.md`。
59
+
60
+ 何时运行、怎样关闭:
61
+
62
+ - 单独使用本技能时,`review.py` 只在用户或 Agent 显式执行它时运行;不执行它就没有任何外发。用户不希望稿件离开本机时,不要运行它,改为人工复核后直接跑 `validate.py`。
63
+ - 端到端编排默认在 TTS 前自动调用一次这项评审,失败不阻断;关闭方式是编排入口的 `--no-review-narration` 或环境变量 `REVIEW_NARRATION=0`。若同时开启了严格评审(`--require-narration-review` 或 `REQUIRE_NARRATION_REVIEW=1`),评审必须运行,关闭开关不生效。
64
+ - 评审是建议型的:只写报告,从不改写 `narration.json`;是否修改由 Agent 或用户决定。
65
+
66
+ ## 3. 读取素材并确认状态
41
67
 
42
68
  首先阅读:
43
69
 
44
70
  - `work_dir/agent_narration_brief.md`:场景、时长、安静窗口与字数预算。
45
71
  - `asr_writing_chunks.json`:长对白的写作分块。
46
72
  - `timeline_fusion.json`:判断某段是否有对白或静音槽。
47
- - `vlm_analysis.json` / `asr_result.json`:核对具体画面与原声证据。
73
+ - `vlm_analysis.json` / `asr_result.json`:核对具体画面与原声证据;有 `asr_clean.json` 时以它的文本为准(lint、评审、剪辑和合成都读它)。
48
74
  - brief 顶部列出的 contact sheet:不要只依赖场景摘要;反应、走位、静止和台词前后的具体时刻常常更重要。
75
+ - `production_reference.json`(仅当 `work_dir` 里有):另一部成片拆出的可迁移方法与节奏数值,用法见 §4。
49
76
 
50
77
  full 模式使用原片时间。cut 模式第一阶段只写 `clip_plan.json`;`edited_source.mp4` 产生后,第二阶段才按输出时间写 `narration.json`。
51
78
 
@@ -58,7 +85,7 @@ full 模式使用原片时间。cut 模式第一阶段只写 `clip_plan.json`;
58
85
 
59
86
  必须确认旁白没有跨越错误剪辑边界,也没有落进已删除区间。整个判断只依赖 `work_dir` 产物。
60
87
 
61
- ## 3. 制定创作方案
88
+ ## 4. 制定创作方案
62
89
 
63
90
  先阅读 `references/creative-editing-playbook.md`,再按创作控制模式写或更新工作产物:
64
91
 
@@ -68,7 +95,9 @@ full 模式使用原片时间。cut 模式第一阶段只写 `clip_plan.json`;
68
95
 
69
96
  只记录决定、证据锚点、被放弃的备选方案和简短理由,不写冗长思维过程。
70
97
 
71
- ### 3.1 导演判断
98
+ 若 `work_dir` 有 `production_reference.json`,制定方案前先读它。它来自另一部成片,只含可迁移的方法和测得的节奏,不含本片事实,不能作为本片画面、剧情或台词的证据。优先级:用户指令 > 本片证据 > 参考。CREATE 可把它的 `structure` 当作一个候选假设,与素材自生的假设比较;DIRECTED / REVISION 默认不套用。`targets` 是参考值,不是配额:与本片的 `audio_owner`、完整台词或表演冲突时以素材为准。可在 `recap_story_plan.json` 写可选字段 `reference_methods: [{"id": "m1", "decision": "adopt|adapt|skip", "note": "…"}]`。没有这个文件就跳过本段。
99
+
100
+ ### 4.1 导演判断
72
101
 
73
102
  锁定:
74
103
 
@@ -79,11 +108,11 @@ full 模式使用原片时间。cut 模式第一阶段只写 `clip_plan.json`;
79
108
  - 隐瞒与揭示
80
109
  - 结尾余味
81
110
 
82
- ### 3.2 故事编辑
111
+ ### 4.2 故事编辑
83
112
 
84
113
  CREATE 比较两个真正可行的结构后选择一个;DIRECTED / REVISION 沿用用户指定或已确认的结构,除非最新反馈明确改变故事方向。每个 beat 至少改变一项:知识、权力、目标、关系、情绪或风险。若删除后因果、人物和情绪都没有损失,该 beat 通常不应保留。
85
114
 
86
- ### 3.3 画面剪辑
115
+ ### 4.3 画面剪辑
87
116
 
88
117
  选择具体时刻,而不是只选择事件。比较:
89
118
 
@@ -94,7 +123,7 @@ CREATE 比较两个真正可行的结构后选择一个;DIRECTED / REVISION
94
123
 
95
124
  在不破坏理解的前提下晚进早出,同时保留不可替代的表演、停顿、失误、动作声和完整台词。
96
125
 
97
- ### 3.4 声音与旁白分工
126
+ ### 4.4 声音与旁白分工
98
127
 
99
128
  先指定 `audio_owner`,再写字。旁白只允许承担以下 `narration_job`:
100
129
 
@@ -107,7 +136,7 @@ CREATE 比较两个真正可行的结构后选择一个;DIRECTED / REVISION
107
136
 
108
137
  画面、原声或沉默已经足够时使用 `none`,不要默认铺旁白。
109
138
 
110
- ### 3.5 cut 模式第一阶段
139
+ ### 4.5 cut 模式第一阶段
111
140
 
112
141
  cut 模式先根据 `recap_story_plan.json` 与 `visual_audio_board.json` 写原片时间的 `clip_plan.json`,此时不要写 `narration.json`:
113
142
 
@@ -132,7 +161,7 @@ beat_id | function | change | POV | preferred moment | 入点 | 出点
132
161
 
133
162
  片段顺序必须构成一条完整故事线,而不是无序高光。可使用 0–1 个 cold open,随后回到因果清楚的 setup → turn → escalation → payoff。片段长度服从具体时刻,不使用统一秒数模板;片尾必须保留完整台词或动作。密集 scene-change 候选的来源判断与处理规则由剪辑阶段定义,写计划时遵循同一规则,不制造人工闪切。
134
163
 
135
- ## 4. 撰写旁白
164
+ ## 5. 撰写旁白
136
165
 
137
166
  full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.mp4` 与剪后故事板,补充 `visual_audio_board.json` 的输出时间并重新确认 `audio_owner` / `narration_job`,再按输出时间写:
138
167
 
@@ -153,25 +182,25 @@ full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.m
153
182
 
154
183
  | 字段 | 含义 |
155
184
  |------|------|
156
- | `start` / `end` | full 模式为原片时间;cut 第二阶段为输出时间 |
185
+ | `start` / `end` | full 模式为原片时间;cut 第二阶段为输出时间。与上一块间隔不超过 1.6 秒的块属于同一段落,assemble 会让它紧接上一块的实际结尾(间隔 0.35 秒)播放,最多比 `start` 提前 1.2 秒,且提前的那一段不会进入原声对白;段落首块从 `start` 开始,上一块超时则顺延到上一块结尾加停顿之后 |
157
186
  | `narration` | 解说文本 |
158
187
  | `pause_after_ms` | 段后停顿,默认 250ms |
159
188
  | `overlaps_speech` | 是否与原对白重叠;连续铺底窗口通常为 `true`,真正静音槽才为 `false` |
160
189
  | `emotion` | 整个解说块的 MiMo TTS 情绪/语气标签 |
161
190
 
162
- ### 4.1 写作规则
191
+ ### 5.1 写作规则
163
192
 
164
193
  1. **先有 `narration_job`,后有句子**:没有明确任务就不写;旁白不是默认音轨。
165
194
  2. **按连续思路写**:旁白拥有一个 beat 时,用一个或少量完整句子完成“前提 → 触发动作 → 变化/意义”,并在一次 TTS 中合成。句号服从口语思路和呼吸,不服从字幕换行;不要固定句数,也不要“一句一停”。
166
195
  3. **7:3 不是配额**:只在素材判断不足时作为避免墙到墙旁白的粗略首稿参考。实际比例服从 `audio_owner`;强对白、动作声或沉默可以完整拥有一个 beat。
167
196
  4. **视听接力**:旁白若引出原声,块尾要让观众想听;原声结束后的下一块要承接它造成的变化。
168
- 5. **按有效语速控量**:用 `字数 / brief 头部 speech budget` 估算窗口;装不下时删减或拆分叙事任务,不用加速堆字。
197
+ 5. **按有效语速控量**:用 `字数 / brief 头部 speech budget` 估算朗读秒数,每块再加约 0.45 秒 TTS 首尾静音。brief 里每个窗口标的字数没扣这段静音,2–3 秒的短窗口要比它少写一两个字;装不下时删减或拆分叙事任务,不用加速堆字。
169
198
  6. **不看图说话**:旁白只增加上下文、因果、预期、证据支持的解释或跨越。
170
199
  7. **人物与证据优先**:优先使用已知角色名;关系、动机、潜台词和结果必须指向 visual / ASR / research / user context,且不能把背景资料伪装成当前画面事实。
171
200
  8. **写给耳朵听**:使用具体名词和动词,句子完整、口语可听;避免字幕腔、半句、空泛拔高和破折号。TTS 文本先保证听感连续,字幕再按阅读宽度拆分,不能反过来把朗读稿切碎。
172
201
  9. **避免模板化纠偏**:“不是 A,而是 B”只在确实存在一个观众可能相信、而素材又要纠正的判断时使用。它不是禁句,但不能靠先否定再肯定制造假洞察;优先直接写人物的动作、因果和后果。
173
202
 
174
- ### 4.2 解说结构
203
+ ### 5.2 解说结构
175
204
 
176
205
  - **钩子**:提出正文会真实兑现的问题或利害,不用无关留存话术。
177
206
  - **主线**:围绕选定 POV 与主线推进,不在每个场景重新开篇。
@@ -180,7 +209,7 @@ full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.m
180
209
  - **收尾**:回答或有意转化开头问题,留下明确余味。
181
210
  - **衔接**:旁白块与相邻原声属于同一个 beat,前者铺垫、后者呈现、下一块承接。
182
211
 
183
- ### 4.3 原声留白字幕
212
+ ### 5.3 原声留白字幕
184
213
 
185
214
  可选写 `original_subtitles.json`,使用成片输出时间:
186
215
 
@@ -190,7 +219,7 @@ full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.m
190
219
 
191
220
  只写留白中实际听得到的台词,订正 ASR 错字与人名,每条尽量控制在一行;被旁白盖住或已经剪掉的句子不要写。省略时,合成阶段会使用保守的 ASR 映射兜底,并在成片中用 `「」` 区分原声对白与旁白。
192
221
 
193
- ## 5. 创作自审
222
+ ## 6. 创作自审
194
223
 
195
224
  在调用 LLM 评审前做以下**反事实检查**:
196
225
 
@@ -206,19 +235,19 @@ full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.m
206
235
 
207
236
  REVISION 还要逐项确认:用户点名的问题已经改变,未点名的冻结项没有意外变化,相关 `style_card.json` / `visual_audio_board.json` 中不存在旧镜头或旧表达。除非用户要求备选版本,不额外扩展新方向。
208
237
 
209
- ## 6. 评审与校验
238
+ ## 7. 评审与校验
210
239
 
211
- ### 6.1 建议型语义评审
240
+ ### 7.1 建议型语义评审
212
241
 
213
242
  ```bash
214
- python3 scripts/review.py --work-dir <work_dir>
243
+ python3 scripts/review.py --work-dir <work_dir> # 会联网:外发内容与关闭方式见 §2
215
244
  ```
216
245
 
217
246
  评审会自动识别 cut 模式,并在存在已校验剪辑计划时按输出时间线核对;`--timeline source` 可强制使用原片时间。打开 `narration_review.md`,逐项处理 `error`,尤其是 `category=hallucination`。
218
247
 
219
248
  重复修改并评审,直到:
220
249
 
221
- - `verdict` 为 `PASS` / `OK` 且没有 `error`;或
250
+ - `verdict` 为 `PASS` 且没有 `error`;或
222
251
  - 对仍保留的问题做明确 override。
223
252
 
224
253
  覆盖决定追加到 `work_dir/narration_review_override.md`:
@@ -233,19 +262,19 @@ python3 scripts/review.py --work-dir <work_dir>
233
262
 
234
263
  `review.py` 本身只写报告,默认调用策略为建议型、失败开放;若调用方显式开启严格评审,事实矛盾、残句、解析失败或评审不可用可在 TTS 前阻断。覆盖记录只用于审计,`review.py` / `validate.py` 不读取它。
235
264
 
236
- ### 6.2 确定性硬校验
265
+ ### 7.2 确定性硬校验
237
266
 
238
267
  ```bash
239
268
  python3 scripts/validate.py --work-dir <work_dir> --mode full
240
- python3 scripts/validate.py --work-dir <work_dir> --mode full --preserve-approved-text
241
269
  # cut 输出时间线由编排器使用 --mode cut_output
242
270
  ```
243
271
 
244
- 命令写出 `narration_lint.json`。full 模式默认执行字符预算整理、去重合并并依据安静窗口计算 `overlaps_speech`,但不会把时间段移动到安静窗口;`--preserve-approved-text` 则保留批准稿的原始时间、文本、停顿、顺序与扩展元数据,只允许更新实测 `overlaps_speech`。修复所有 error 后重复运行,直到校验干净,再继续 TTS 与合成。
272
+ 命令写出 `narration_lint.json`。full 与 cut_output 用同一套声音归属算法(原声对白区间减去安静窗口)回写 `overlaps_speech`,其余字段原样保留,不截短、不合并、不补标点、不重排。推荐字数按时间窗先扣 0.45 秒 TTS 首尾静音再计算;full 模式下某段字数超过推荐字数的 1.25 倍即报 `over_budget` error,报告里写明时间窗、`budget_chars`、`limit_chars`、`actual_chars` 和 `over_chars`:缩短文字或放宽/挪动时间窗,不要指望 TTS 替你缩稿。有 error 时命令以非零退出,逐块列出「段 N」(narration.json 里第 N 块,从 1 数;`narration_lint.json` 的 `index` 仍从 0 数)、错误码、关键数字和改法。修复所有 error 后重复运行,直到校验干净,再继续 TTS 与合成。
245
273
 
246
274
  片名或题材明确但缺少剧情上下文时,先按本技能的 `references/research-guide.md` 写 `background_research.json`。若理解素材偏薄,brief 中的数量只能当上限:宁可少写、写实,也不要为凑数复述画面。
247
275
 
248
- ## 7. 能力边界
276
+ ## 8. 能力边界
249
277
 
250
278
  - 不运行 ASR / VLM,不合成 TTS,不渲染视频;只消费视频理解索引。
279
+ - 唯一的远程调用是 `review.py` 的 MiMo 文字评审(见 §2);`validate.py` 与 lint 只在本地运行。
251
280
  - 平台研究仅用于明确的宣发任务;不替代当前片内事实,也不默认改变解说和剪辑。
@@ -1,19 +1,8 @@
1
- """Normalize, split, budget, and de-duplicate narration text."""
1
+ """Count and budget agent-written narration text for lint."""
2
2
 
3
- import copy
4
3
  import re
5
4
 
6
- from lib import CONFIG, log
7
- from deslop_qc import _sentence_pieces, _text_units
8
-
9
-
10
- def _format_frame_facts(scene):
11
- """将帧动作描述格式化为可注入 agent brief 的文本。"""
12
- facts = scene.get("frame_facts", {})
13
- if not facts:
14
- return ""
15
- lines = [f" {ts}s: {'; '.join(facts[ts])}" for ts in sorted(facts, key=float)]
16
- return "\n 帧动作:\n" + "\n".join(lines)
5
+ from lib import CONFIG
17
6
 
18
7
 
19
8
  def _text_char_count(text):
@@ -27,214 +16,12 @@ def _text_char_count(text):
27
16
  )
28
17
 
29
18
 
30
- def _split_text_by_sentence_windows(text, min_chars=500, max_chars=800):
31
- """Clipto-style three-tier sentence boundary splitting for long ASR text."""
32
- text = re.sub(r"\s+", " ", text).strip()
33
- if not text:
34
- return []
35
- if _text_units(text) <= max_chars:
36
- return _sentence_pieces(text)
37
-
38
- result = []
39
- rest = text
40
- sentence_marks = "。!?!?;;."
41
- while _text_units(rest) > max_chars:
42
- # The min/max thresholds are unit-based but punctuation is char-indexed.
43
- # For Chinese (the dominant recap target) these are nearly identical; for
44
- # non-CJK this remains a safe sentence-boundary heuristic around words.
45
- char_min = min(len(rest), max(1, min_chars))
46
- char_max = min(len(rest), max(1, max_chars))
47
- window = rest[:char_max]
48
- cut = max(window.rfind(mark) for mark in sentence_marks)
49
- if cut + 1 < char_min:
50
- outside = -1
51
- for i, ch in enumerate(rest[char_max:], start=char_max):
52
- if ch in sentence_marks:
53
- outside = i
54
- break
55
- cut = outside if outside >= 0 and outside + 1 <= len(rest) else char_max - 1
56
- piece = rest[: cut + 1].strip()
57
- if not piece:
58
- piece = rest[:char_max].strip()
59
- cut = char_max - 1
60
- result.append(piece)
61
- rest = rest[cut + 1 :].strip()
62
- if rest:
63
- result.append(rest)
64
- return result
65
-
66
-
67
- def _timed_sentence_pieces(seg, min_chars, max_chars):
68
- """Split one ASR segment into timed sentence pieces with approximate spans."""
69
- text = seg["text"].strip()
70
- if not text:
71
- return []
72
- start, end = seg["start"], seg["end"]
73
- pieces = []
74
- for sentence in _sentence_pieces(text):
75
- if _text_units(sentence) > max_chars:
76
- pieces.extend(
77
- _split_text_by_sentence_windows(
78
- sentence, min_chars=min_chars, max_chars=max_chars
79
- )
80
- )
81
- else:
82
- pieces.append(sentence)
83
- total_units = sum(max(1, _text_units(piece)) for piece in pieces)
84
- duration = end - start
85
- cursor = start
86
- timed = []
87
- for idx, piece in enumerate(pieces):
88
- units = max(1, _text_units(piece))
89
- piece_end = end if idx == len(pieces) - 1 else cursor + duration * units / total_units
90
- timed.append(
91
- {
92
- "start": round(cursor, 2),
93
- "end": round(piece_end, 2),
94
- "text": piece,
95
- "char_count": _text_units(piece),
96
- }
97
- )
98
- cursor = piece_end
99
- return timed
100
-
101
-
102
- def _scene_ids_for_range(scenes, start, end):
103
- duration = max(0.001, end - start)
104
- scene_ids = []
105
- for scene in scenes:
106
- overlap = _overlap_seconds(start, end, scene["start"], scene["end"])
107
- # Ignore tiny boundary tails from approximate ASR sentence timing. A
108
- # scene id should mean the chunk materially belongs to that scene.
109
- if overlap and (overlap >= duration * 0.2 or overlap >= 3.0):
110
- scene_ids.append(scene["scene_id"])
111
- return scene_ids
112
-
113
-
114
- def _chunk_asr_for_writing(segments, scenes_analysis, min_chars=None, max_chars=None):
115
- """Chunk ASR into semantic windows before an agent writes long-dialogue recaps.
116
-
117
- The strategy mirrors Clipto's segment splitter: accumulate a window, prefer
118
- the last sentence boundary inside max length, allow a slightly longer first
119
- boundary outside the window, and fall back to the remaining text. CJK text is
120
- measured by characters; non-CJK text is measured by words.
121
- """
122
- min_chars = CONFIG["asr_chunk_min_chars"] if min_chars is None else min_chars
123
- max_chars = CONFIG["asr_chunk_max_chars"] if max_chars is None else max_chars
124
- pieces = [
125
- piece
126
- for seg in segments
127
- for piece in _timed_sentence_pieces(seg, min_chars, max_chars)
128
- ]
129
-
130
- chunks = []
131
- current = []
132
- current_units = 0
133
- current_scene_ids = set()
134
-
135
- def flush():
136
- nonlocal current, current_units, current_scene_ids
137
- if not current:
138
- return
139
- chunks.append(
140
- {
141
- "chunk_id": len(chunks),
142
- "start": current[0]["start"],
143
- "end": current[-1]["end"],
144
- "scene_ids": sorted(
145
- current_scene_ids, key=lambda sid: (isinstance(sid, str), sid)
146
- ),
147
- "char_count": current_units,
148
- "text": " ".join(piece["text"] for piece in current).strip(),
149
- "segments": current,
150
- }
151
- )
152
- current = []
153
- current_units = 0
154
- current_scene_ids = set()
155
-
156
- for piece in pieces:
157
- units = max(1, piece["char_count"])
158
- piece_scene_ids = set(
159
- _scene_ids_for_range(scenes_analysis, piece["start"], piece["end"])
160
- )
161
- crosses_scene = (
162
- current
163
- and current_scene_ids
164
- and piece_scene_ids
165
- and not (current_scene_ids & piece_scene_ids)
166
- )
167
- if crosses_scene and current_units >= min_chars:
168
- flush()
169
- if current and current_units >= min_chars and current_units + units > max_chars:
170
- flush()
171
- current.append(piece)
172
- current_scene_ids.update(piece_scene_ids)
173
- current_units += units
174
- if current_units >= max_chars:
175
- flush()
176
- flush()
177
- return chunks
178
-
179
-
180
- def _truncate_at_sentence(text, max_chars):
181
- """在句子边界截断,不产生残句。max_chars 按有效字符计(不含标点空白)。"""
182
- if _text_char_count(text) <= max_chars:
183
- return text
184
- eff = 0
185
- cutoff = len(text)
186
- for i, ch in enumerate(text):
187
- eff += 1 if _text_char_count(ch) else 0
188
- if eff > max_chars:
189
- cutoff = i + 1
190
- break
191
- idx = max(text[:cutoff].rfind(sep) for sep in ["。", "!", "?", "!", "?"])
192
- if idx > 0:
193
- return text[: idx + 1]
194
- idx = max(text[:cutoff].rfind(sep) for sep in [",", "、", ";", ","])
195
- if idx > 3:
196
- return text[:idx] + "。"
197
- return ""
198
-
199
-
200
- def _char_bigrams(text):
201
- return {text[i : i + 2] for i in range(len(text) - 1) if text[i : i + 2].strip()}
202
-
203
-
204
- def _post_dedup_narration(narration):
205
- """去除相邻相似解说段(bigram 重叠 >60% 则合并)。"""
206
- if len(narration) < 2:
207
- return narration
208
- result = [narration[0]]
209
- for seg in narration[1:]:
210
- prev = result[-1]
211
- set_a, set_b = _char_bigrams(prev["narration"]), _char_bigrams(seg["narration"])
212
- if not set_a or not set_b:
213
- result.append(seg)
214
- continue
215
- overlap = len(set_a & set_b) / min(len(set_a), len(set_b))
216
- # Only merge near-identical adjacent beats. Short Chinese beats share many
217
- # bigrams by chance, so a low threshold collapses intentional parallel beats
218
- # ("他不再试探" / "他直接赌上全力") and silently drops density below target.
219
- if overlap > 0.6:
220
- # Validation is also the handoff boundary for renderer metadata: keep the
221
- # overlays authored on either merged beat.
222
- merged_overlays = copy.deepcopy(
223
- prev.get("visual_overlays", []) + seg.get("visual_overlays", [])
224
- )
225
- if len(seg["narration"]) > len(prev["narration"]):
226
- prev["narration"] = seg["narration"]
227
- prev["end"] = seg["end"]
228
- prev["pause_after_ms"] = seg["pause_after_ms"]
229
- if merged_overlays:
230
- prev["visual_overlays"] = merged_overlays
231
- log(f" 去重合并: {prev['start']:.0f}-{prev['end']:.0f}s")
232
- else:
233
- result.append(seg)
234
- removed = len(narration) - len(result)
235
- if removed:
236
- log(f" 去重: {len(narration)} → {len(result)} 段 (合并 {removed} 段)")
237
- return result
19
+ def _find_scene_for_midpoint(scenes_analysis, start, end):
20
+ mid = (start + end) / 2
21
+ for scene in scenes_analysis:
22
+ if scene["start"] <= mid <= scene["end"]:
23
+ return scene
24
+ return None
238
25
 
239
26
 
240
27
  def _scene_available_seconds(start, end):
@@ -251,51 +38,15 @@ def _recommended_char_budget(start, end):
251
38
  return int(_scene_available_seconds(start, end) * effective_rate)
252
39
 
253
40
 
254
- def _find_scene_for_midpoint(scenes_analysis, start, end):
255
- mid = (start + end) / 2
256
- for scene in scenes_analysis:
257
- if scene["start"] <= mid <= scene["end"]:
258
- return scene
259
- return None
260
-
261
-
262
- def _normalise_narration_segment(seg):
263
- """Normalize one lint-validated narration segment for the full-mode rewrite.
264
-
265
- Authored timing is a delivery contract: scene boundaries are approximate
266
- visual-analysis buckets, so start/end are never clamped here.
267
- """
268
- item = {
269
- "start": round(seg["start"], 2),
270
- "end": round(seg["end"], 2),
271
- "narration": str(seg["narration"]).strip(),
272
- "pause_after_ms": int(seg.get("pause_after_ms", CONFIG["breath_ms"])),
273
- "overlaps_speech": bool(seg.get("overlaps_speech", True)),
274
- }
275
- for optional_key in (
276
- "source_start",
277
- "source_end",
278
- "source_clip_id",
279
- "source_entry_policy",
280
- "source_entry_reason",
281
- ):
282
- if optional_key in seg:
283
- item[optional_key] = seg[optional_key]
284
- # carry the per-beat emotion/tone tag (MiMo TTS instruct) through lint untouched
285
- if seg.get("emotion"):
286
- item["emotion"] = seg["emotion"].strip()
287
- # Renderer-owned metadata survives the validation rewrite; the recap orchestrator
288
- # filters supported overlay kinds later.
289
- if "visual_overlays" in seg:
290
- item["visual_overlays"] = copy.deepcopy(seg["visual_overlays"])
291
- return item
292
-
293
-
294
- def _clean_narration_punctuation(text):
295
- text = re.sub(r"\s+", " ", text).strip()
296
- text = re.sub(r'[,:、;,]["\']?[。!?]', "。", text)
297
- return re.sub(r'["\']。$', "。", text)
41
+ # Lint-only realism on top of the brief's budget. Every block is one TTS utterance, and the
42
+ # provider pads each utterance with edge silence that voiceover keeps and assemble must
43
+ # place inside the window. Measured on 98 MiMo blocks from real runs (-40 dBFS): 0.40 /
44
+ # 0.54 / 0.62 s at p10 / p50 / p90 before the 1.15x narration atempo, i.e. about 0.47 s
45
+ # placed. On a 2-3 s window that is a fifth of the room, so without it a short window
46
+ # passed lint and failed in assemble as no_safe_fit after TTS was billed.
47
+ TTS_UTTERANCE_OVERHEAD_SECONDS = 0.45
298
48
 
299
49
 
300
- def _overlap_seconds(start, end, other_start, other_end):
301
- return max(0.0, min(end, other_end) - max(start, other_start))
50
+ def _lint_char_budget(start, end):
51
+ """The brief's budget for the window minus one utterance's TTS edge silence."""
52
+ return _recommended_char_budget(start, end - TTS_UTTERANCE_OVERHEAD_SECONDS)
@@ -61,7 +61,11 @@ def _text_units(text: str) -> int:
61
61
  def _normalise_segments(payload: list[dict[str, Any]], *, source: str) -> list[dict[str, Any]]:
62
62
  key = "narration" if source == "narration" else "text"
63
63
  segments: list[dict[str, Any]] = []
64
+ # `index` is the item's position in the file as written (non-objects included), so a
65
+ # blocker points at the same entry the author sees there.
64
66
  for idx, item in enumerate(payload):
67
+ if not isinstance(item, dict):
68
+ continue
65
69
  text = str(item.get(key, "")).strip()
66
70
  if text:
67
71
  segments.append({"source": source, "index": idx, "text": text})
@@ -77,46 +81,27 @@ def _load_original_subtitles(work_dir: Path | None) -> list[dict[str, Any]]:
77
81
  return _normalise_segments(json.loads(path.read_text(encoding="utf-8")), source="original_subtitles")
78
82
 
79
83
 
80
- def _style_card_requirement(work_dir: Path | None) -> tuple[bool, str]:
81
- """Read the explicit requirements contract; workspaces without one are advisory-only."""
82
- if work_dir is None:
83
- return False, "legacy_default"
84
- path = work_dir / "deslop_qc_requirements.json"
85
- if not path.exists():
86
- return False, "legacy_default"
87
- data = json.loads(path.read_text(encoding="utf-8"))
88
- return data["style_card_required"], "deslop_qc_requirements.json"
89
-
90
-
91
- def _style_card_issue(work_dir: Path | None, required: bool) -> dict[str, Any] | None:
84
+ def _style_card_issue(work_dir: Path | None) -> dict[str, Any] | None:
85
+ """A missing or empty style_card.json is always advisory, never a render blocker."""
92
86
  if work_dir is None:
93
87
  return None
94
- severity = "blocker" if required else "advisory"
95
88
  path = work_dir / "style_card.json"
96
89
  if not path.exists():
97
90
  return {
98
- "severity": severity,
91
+ "severity": "advisory",
99
92
  "code": "missing_style_card",
100
93
  "source": "style_card",
101
94
  "index": None,
102
- "message": (
103
- "style_card.json is required by this expression/packaging run but is missing"
104
- if required else
105
- "style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it"
106
- ),
95
+ "message": "style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it",
107
96
  }
108
97
  data = json.loads(path.read_text(encoding="utf-8"))
109
98
  if not isinstance(data, dict) or not data:
110
99
  return {
111
- "severity": severity,
100
+ "severity": "advisory",
112
101
  "code": "malformed_style_card",
113
102
  "source": "style_card",
114
103
  "index": None,
115
- "message": (
116
- "style_card.json is required but empty or not a JSON object"
117
- if required else
118
- "style_card.json is present but empty or not a JSON object; treat as migration warning unless the run requires it"
119
- ),
104
+ "message": "style_card.json is present but empty or not a JSON object; treat as migration warning",
120
105
  }
121
106
  return None
122
107
 
@@ -139,10 +124,9 @@ def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path |
139
124
  blockers: list[dict[str, Any]] = []
140
125
  advisories: list[dict[str, Any]] = []
141
126
 
142
- required, requirement_source = _style_card_requirement(work_path)
143
- style_issue = _style_card_issue(work_path, required)
127
+ style_issue = _style_card_issue(work_path)
144
128
  if style_issue:
145
- (blockers if style_issue["severity"] == "blocker" else advisories).append(style_issue)
129
+ advisories.append(style_issue)
146
130
 
147
131
  all_text = "\n".join(seg["text"] for seg in segments)
148
132
  total_units = max(1, _text_units(all_text))
@@ -189,8 +173,6 @@ def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path |
189
173
  "ok": not blockers,
190
174
  "contract": CONTRACT,
191
175
  "scanner": "deslop_qc.py",
192
- "style_card_required": required,
193
- "style_card_requirement_source": requirement_source,
194
176
  "blocker_count": len(blockers),
195
177
  "advisory_count": len(advisories),
196
178
  "blockers": blockers,