@oh-story/dsh 0.1.12 → 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
@@ -1,6 +1,7 @@
1
1
  """Load measured source-speech evidence and classify narration ownership."""
2
2
 
3
3
  import json
4
+ import re
4
5
  from pathlib import Path
5
6
 
6
7
  from lib import CONFIG, file_identity
@@ -10,16 +11,93 @@ def _empty_evidence(mode):
10
11
  return {
11
12
  "anchors": [],
12
13
  "speech_spans": [],
14
+ "dialogue_spans": [],
13
15
  "quiet_windows": [],
14
16
  "require_measured": mode == "cut_output",
15
17
  }
16
18
 
17
19
 
20
+ # Interjections and common ASR artifacts on screams/music. A window whose text is only these
21
+ # is not dialogue at a clip edge or narration entry; real short lines such as "救我!" still are.
22
+ # Same copy in video-cut, video-script and video-assemble (parity-tested by function).
23
+ _NON_DIALOGUE_TOKENS = frozenset(
24
+ "啊 嗯 哼 哦 呃 唉 嘿 呦 哈 呀 hi yeah ok okay oh uh ah hmm".split()
25
+ )
26
+ _NON_DIALOGUE_CJK = frozenset("啊嗯哼哦呃唉嘿呦哈呀")
27
+ # Lines cross 15s ASR window edges (a line may run 13.2–15.4 while its window ends at 15.0),
28
+ # so an interjection-only window next to real dialogue keeps this much of its shared edge.
29
+ _INTERJECTION_GUARD_SECONDS = 1.0
30
+
31
+
32
+ def _interjection_only(text):
33
+ tokens = [token for token in re.split(r"[\W_]+", text.lower()) if token]
34
+ # Punctuation-only rows ("……", "?") are often ASR for unintelligible speech: keep them.
35
+ return bool(tokens) and all(
36
+ token in _NON_DIALOGUE_TOKENS or set(token) <= _NON_DIALOGUE_CJK for token in tokens
37
+ )
38
+
39
+
40
+ def _dialogue_speech_spans(rows):
41
+ """Merged dialogue spans from timed ASR rows.
42
+
43
+ A row holding only interjections ("啊!", "Hi.") is not dialogue, except a
44
+ `_INTERJECTION_GUARD_SECONDS` guard on an edge it shares with a dialogue row. A row
45
+ whose text is empty or whitespace (ASR heard no words) is timing-only evidence: it is
46
+ skipped here and guards nothing. A row with no `text` field is measured timing whose
47
+ words are unknown and counts as dialogue.
48
+ """
49
+ rows = sorted(
50
+ (
51
+ {
52
+ "start": row["start"],
53
+ "end": row["end"],
54
+ "dialogue": not _interjection_only(row.get("text", "")),
55
+ }
56
+ for row in rows
57
+ if "text" not in row or row["text"].strip()
58
+ ),
59
+ key=lambda row: (row["start"], row["end"]),
60
+ )
61
+ spans = []
62
+ for idx, row in enumerate(rows):
63
+ if row["dialogue"]:
64
+ spans.append({"start": row["start"], "end": row["end"]})
65
+ continue
66
+ before = rows[idx - 1] if idx > 0 else None
67
+ after = rows[idx + 1] if idx + 1 < len(rows) else None
68
+ if before and before["dialogue"] and row["start"] - before["end"] <= 0.05:
69
+ end = min(row["end"], row["start"] + _INTERJECTION_GUARD_SECONDS)
70
+ spans.append({"start": row["start"], "end": end})
71
+ if after and after["dialogue"] and after["start"] - row["end"] <= 0.05:
72
+ start = max(row["start"], row["end"] - _INTERJECTION_GUARD_SECONDS)
73
+ spans.append({"start": start, "end": row["end"]})
74
+ spans.sort(key=lambda row: (row["start"], row["end"]))
75
+ merged = []
76
+ for span in spans:
77
+ if merged and span["start"] <= merged[-1]["end"] + 0.05:
78
+ merged[-1]["end"] = max(merged[-1]["end"], span["end"])
79
+ else:
80
+ merged.append(span)
81
+ return merged
82
+
83
+
18
84
  def _read_json(path):
19
85
  """JSON artifact, or None when the optional file was never written."""
20
86
  return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None
21
87
 
22
88
 
89
+ def _source_asr_rows(work_dir):
90
+ """Cleaned ASR segments (asr_clean.json) when present, else raw asr_result.json.
91
+
92
+ Same precedence as video-cut's edge gate and video-assemble's entry check, so one
93
+ transcript decides lint, cut and assemble verdicts. Blank-text rows are timing-only
94
+ evidence (ASR heard no words) and are not source speech.
95
+ """
96
+ clean = _read_json(work_dir / "asr_clean.json")
97
+ rows = clean["segments"] if clean is not None else _read_json(work_dir / "asr_result.json")
98
+ return [row for row in rows or [] if row["text"].strip()]
99
+
100
+
23
101
  def _output_payload_is_current(payload, work_dir):
24
102
  plan_path = Path(work_dir) / "clip_plan_validated.json"
25
103
  return (
@@ -38,9 +116,7 @@ def load_source_sentence_evidence(work_dir, mode="full"):
38
116
  payload = _read_json(work_dir / "speech_boundary_anchors.json") or {
39
117
  "sentence_anchors": []
40
118
  }
41
- speech_spans = [
42
- row for row in _read_json(work_dir / "asr_result.json") or [] if row["text"]
43
- ]
119
+ speech_spans = _source_asr_rows(work_dir)
44
120
  quiet_windows = [
45
121
  row
46
122
  for row in _read_json(work_dir / "silence_periods.json") or []
@@ -54,14 +130,22 @@ def load_source_sentence_evidence(work_dir, mode="full"):
54
130
  payload = {"sentence_anchors": [], "speech_spans": [], "quiet_windows": []}
55
131
  speech_spans = payload["speech_spans"]
56
132
  quiet_windows = payload["quiet_windows"]
133
+ # `boundary_use` (schema 2) keeps coarse-ASR estimates usable as `unverified`; schema-1
134
+ # anchors (no `boundary_use`) were all coarse estimates: high/medium are `unverified`.
57
135
  anchors = [
58
136
  anchor
59
137
  for anchor in payload["sentence_anchors"]
60
- if anchor["confidence"] in {"high", "medium"}
138
+ if (
139
+ anchor.get("boundary_use")
140
+ or ("unverified" if anchor["confidence"] in {"high", "medium"} else "none")
141
+ )
142
+ != "none"
61
143
  ]
62
144
  return {
63
145
  "anchors": sorted(anchors, key=lambda item: item["time"]),
64
146
  "speech_spans": speech_spans,
147
+ # Entry ownership only: an interjection-only window has no sentence to interrupt.
148
+ "dialogue_spans": _dialogue_speech_spans(speech_spans),
65
149
  "quiet_windows": quiet_windows,
66
150
  "require_measured": mode == "cut_output",
67
151
  }
@@ -124,7 +208,7 @@ def entry_overlaps_source_speech(start, evidence, *, authored_overlap=True, tole
124
208
  return False
125
209
  if any(
126
210
  row["start"] - tolerance <= start < row["end"] - tolerance
127
- for row in evidence["speech_spans"]
211
+ for row in evidence["dialogue_spans"]
128
212
  ):
129
213
  return True
130
214
  if evidence["speech_spans"]:
@@ -2,9 +2,9 @@
2
2
  """video-script validation entrypoint.
3
3
 
4
4
  Validate an agent-written narration.json against the local understanding index.
5
- Legacy full mode performs budget cleanup/deduplication and derives speech ownership.
6
- Protected mode keeps the approved timeline and metadata exact except for measured
7
- ``overlaps_speech`` ownership.
5
+ Validation never rewrites the agent's text, timing, order or metadata. Full-mode text
6
+ that does not fit its window fails lint and goes back to the author. full and cut_output
7
+ persist only the measured ``overlaps_speech`` ownership.
8
8
  """
9
9
 
10
10
  import argparse
@@ -12,13 +12,9 @@ import json
12
12
  import math
13
13
  from pathlib import Path
14
14
 
15
- from lib import CONFIG, log
16
- from narration_lint import (
17
- _validate_narration_budget,
18
- validate_narration_or_raise,
19
- )
15
+ from lib import log
16
+ from narration_lint import NarrationLintError, validate_narration_or_raise
20
17
  from speech_ownership import measure_narration_speech_ownership
21
- from timeline_fusion import _align_narration_to_quiet
22
18
 
23
19
 
24
20
  def _load(path):
@@ -57,16 +53,16 @@ def _validate_output_timeline_bounds(narration, duration, tolerance=0.05):
57
53
  start, end = seg["start"], seg["end"]
58
54
  if end <= -tolerance or start >= duration + tolerance:
59
55
  problems.append(
60
- f"segment {idx} [{start:.3f},{end:.3f}] fully outside output_duration={duration:.3f}"
56
+ f"段 {idx + 1} [{start:.3f},{end:.3f}] fully outside output_duration={duration:.3f}"
61
57
  )
62
58
  continue
63
59
  if start < -tolerance:
64
60
  problems.append(
65
- f"segment {idx} start={start:.3f} before output timeline (output_duration={duration:.3f})"
61
+ f"段 {idx + 1} start={start:.3f} before output timeline (output_duration={duration:.3f})"
66
62
  )
67
63
  if end > duration + tolerance:
68
64
  problems.append(
69
- f"segment {idx} end={end:.3f} exceeds output_duration={duration:.3f}"
65
+ f"段 {idx + 1} end={end:.3f} exceeds output_duration={duration:.3f}"
70
66
  )
71
67
  if problems:
72
68
  raise SystemExit(
@@ -76,8 +72,17 @@ def _validate_output_timeline_bounds(narration, duration, tolerance=0.05):
76
72
 
77
73
 
78
74
  def main():
75
+ try:
76
+ _validate(_parse_args())
77
+ except NarrationLintError as exc:
78
+ # A lint failure is an ordinary result the author fixes from the summary,
79
+ # not a crash: exit non-zero with the summary only, no traceback.
80
+ raise SystemExit(str(exc)) from None
81
+
82
+
83
+ def _parse_args():
79
84
  ap = argparse.ArgumentParser(
80
- description="Validate + align agent-written narration.json."
85
+ description="Validate agent-written narration.json and measure speech ownership."
81
86
  )
82
87
  ap.add_argument("--work-dir", required=True)
83
88
  ap.add_argument("--mode", default="full", choices=["full", "cut", "cut_output"])
@@ -87,21 +92,16 @@ def main():
87
92
  default=None,
88
93
  help="cut_output: rendered edited_source.mp4 duration in seconds",
89
94
  )
90
- ap.add_argument(
91
- "--preserve-approved-text",
92
- action="store_true",
93
- help="validate approved narration without rewriting, truncating, merging, or reordering it",
94
- )
95
- args = ap.parse_args()
95
+ return ap.parse_args()
96
96
 
97
+
98
+ def _validate(args):
97
99
  work_dir = Path(args.work_dir)
98
- CONFIG["edit_mode"] = args.mode
99
100
  narration_path = work_dir / "narration.json"
100
101
  narration = _load(narration_path)
101
102
  if narration is None:
102
103
  raise SystemExit(f"缺少 {narration_path};请先按 video-script 规则写解说词")
103
104
  vlm_analysis = _load(work_dir / "vlm_analysis.json")
104
- silence_periods = _load(work_dir / "silence_periods.json") or []
105
105
  if args.mode == "cut_output":
106
106
  # Two-pass cut: narration is authored in OUTPUT time against edited_source.mp4 — there is
107
107
  # no source-time clip membership check. Lint the authored shape first, then derive speech
@@ -109,7 +109,7 @@ def main():
109
109
  # voiceover/assemble instead of trusting JSON.
110
110
  report = validate_narration_or_raise(
111
111
  narration, None, clip_plan=None, mode="cut_output", work_dir=work_dir,
112
- require_chronological=args.preserve_approved_text,
112
+ output_duration=args.output_duration,
113
113
  )
114
114
  narration = measure_narration_speech_ownership(
115
115
  narration, work_dir, mode="cut_output"
@@ -139,21 +139,13 @@ def main():
139
139
  validate_narration_or_raise(
140
140
  narration, vlm_analysis, clip_plan=clip_plan, mode="cut", work_dir=work_dir
141
141
  )
142
- if not args.preserve_approved_text:
143
- narration = _validate_narration_budget(narration, vlm_analysis)
144
142
  else:
143
+ # Same contract as cut_output: lint the authored shape, then persist only the
144
+ # measured speech ownership (speech spans minus quiet windows).
145
145
  validate_narration_or_raise(
146
- narration, vlm_analysis, clip_plan=None, mode="full", work_dir=work_dir,
147
- require_chronological=args.preserve_approved_text,
146
+ narration, vlm_analysis, clip_plan=None, mode="full", work_dir=work_dir
148
147
  )
149
- if args.preserve_approved_text:
150
- narration = measure_narration_speech_ownership(
151
- narration, work_dir, mode="full"
152
- )
153
- else:
154
- narration = _align_narration_to_quiet(
155
- narration, vlm_analysis, silence_periods
156
- )
148
+ narration = measure_narration_speech_ownership(narration, work_dir, mode="full")
157
149
  narration_path.write_text(
158
150
  json.dumps(narration, ensure_ascii=False, indent=2), encoding="utf-8"
159
151
  )
@@ -36,7 +36,7 @@ description: >
36
36
  export MIMO_API_KEY=***
37
37
  ```
38
38
 
39
- ASR 使用 `mimo-v2.5-asr`;VLM 使用 `mimo-v2.5`。`--skip-asr` 可跳过对白转写,但完整理解仍需要 `MIMO_API_KEY` 运行 VLM。`--mimo-video-overview` 可开启按场景块的视频概览。
39
+ ASR 使用 `mimo-v2.5-asr`;VLM 使用 `mimo-v2.5`。`--skip-asr` 可跳过对白转写,但完整理解仍需要 `MIMO_API_KEY` 运行 VLM。`--mimo-video-overview` 可开启按场景块的视频概览。未设置 key 时重跑会复用已缓存的转写、画面分析、概览与故事索引(key 决定的默认 endpoint 不参与比对);需要请求模型的 consolidation 记为 `skipped_no_key`,不发请求。缓存对不上(例如复制 work_dir 时没保留文件时间)而已有转写时,运行停下并保留转写:用 `cp -p` / `cp -Rp` / `rsync -t` 保留时间重新复制,或设置 key 后重跑(会重新转写)。不要用 `--skip-asr` 绕过,它会把现有转写替换成 `[]`。
40
40
 
41
41
  若 `work_dir/background_research.json` 存在,本技能会把剧情梗概和角色名折入 VLM 上下文;`--context` 可补充一条简短提示。
42
42
 
@@ -45,19 +45,44 @@ ASR 使用 `mimo-v2.5-asr`;VLM 使用 `mimo-v2.5`。`--skip-asr` 可跳过对
45
45
  ## 4. 运行命令
46
46
 
47
47
  ```bash
48
- python3 scripts/understand.py <video> --work-dir <work_dir> \
49
- [--context "节目名/角色名"] [--scene-threshold 0.1] [--skip-asr] [--mimo-video-overview] [--force]
48
+ python3 scripts/understand.py <video> --work-dir <work_dir> [选项]
50
49
  ```
51
50
 
51
+ | 选项 | 默认 | 作用 |
52
+ |------|------|------|
53
+ | `<video>` | 必填 | 源视频 |
54
+ | `--work-dir` | 必填 | 产物目录;不存在时创建 |
55
+ | `--context "..."` | 空 | 补充给 VLM 的简短上下文(节目名、角色名),与 `background_research.json` 合并 |
56
+ | `--scene-threshold` | `0.1` | 场景检测阈值 |
57
+ | `--style` | `纪录片` | 写进创作简报的解说风格 |
58
+ | `--edit-mode full\|cut` | 不设 | 写进简报的 recap 模式;`cut` 时按剪后时长估算旁白预算,已有 `edited_source.mp4` 时句末锚点改用剪后时间 |
59
+ | `--target-duration` | 不设 | 写进简报的 cut 目标时长;尚无 `clip_plan_validated.json` 时用它估算旁白预算 |
60
+ | `--skip-asr` | 关 | 不转写对白,把 `asr_result.json` 写成 `[]`(已有转写会被覆盖),ASR 证据标为显式跳过 |
61
+ | `--mimo-video-overview` | 关 | 按场景块运行 MiMo 视频概览,并作为逐场景主描述 |
62
+ | `--force` | 关 | 忽略缓存,全部重算 |
63
+ | `--brief-only` | 关 | 只用现有产物重建 `agent_narration_brief.md`,不抽帧、不调 API |
64
+ | `--edited-storyboard-only` | 关 | 只按 `clip_plan_validated.json` 写剪后时间线 `storyboard/edited_storyboard.*`,并在已有的 `agent_narration_brief.md` 顶部加 storyboard 指引;多源计划(带 `sources`)从各来源 `source_work_dir` 的 `frames/` 按其 `frames_manifest.json` 的 fps 取帧,tile 标 `S1`/`S2`…;不抽帧、不调 API,故事板生成失败只记日志。与 `--brief-only` 互斥 |
65
+ | `--consolidate` / `--no-consolidate` | 开 | 生成全局故事索引 `understanding_index.*` |
66
+ | `--consolidate-asr` | 关 | 另外清洗 ASR 文本,写 `asr_clean.json` |
67
+
52
68
  ## 5. 输出契约
53
69
 
70
+ 默认运行写出下表产物;各阶段产物旁的 `*.meta.json` 是缓存 provenance sidecar。
71
+
54
72
  | 文件 | 内容 |
55
73
  |------|------|
74
+ | `frames/frame_*.jpg`、`frames_manifest.json` | 按 fps 抽出的帧及其清单 |
56
75
  | `scenes.json` | 场景切点、起止时间与时长 |
76
+ | `audio.wav` | 16 kHz 单声道音频,供 ASR、静音检测与句末锚点使用 |
57
77
  | `asr_result.json` | `[{start, end, text}]` 时间戳对白 |
58
78
  | `asr_timing_evidence.json` | ASR 可用性状态、粗窗口精度、glossary 前后文本,以及它所描述的源视频/音频/结果文件(路径存在性 + size/mtime) |
59
- | `vlm_analysis.json` | 逐场景描述、深层分析与 `frame_facts` |
60
79
  | `silence_periods.json` | `[{start, end, duration, has_speech}]` 安静窗口 |
80
+ | `speech_boundary_anchors.json` | ASR 句末标点对齐到短停顿的句末锚点;缺音频或 ASR 时 `status: unavailable` |
81
+ | `vlm_analysis.json` | 逐场景描述、深层分析与 `frame_facts` |
82
+ | `mimo_video_overview.status.json` | 视频概览状态(未启用时为 `disabled`);启用成功另写 `mimo_video_overview.json` |
83
+ | `understanding_index.json`、`understanding_index.md` | 全局故事索引(`--no-consolidate` 时不写) |
84
+ | `consolidation.status.json` | 故事索引与 ASR 清洗的运行状态 |
85
+ | `storyboard/source_storyboard.{json,jpg}` | 原片时间线联系表(超页时续写 `_001.jpg` 等);已有 `clip_plan_validated.json` 时另写 `edited_storyboard.*`;`STORYBOARD=0` 关闭 |
61
86
  | `timeline_fusion.json` | VLM、ASR 与静音信息的统一时间线 |
62
87
  | `asr_writing_chunks.json` | 按句界和场景切分的 ASR 写作块 |
63
88
  | `agent_narration_brief.md` | Agent 首先阅读的创作简报 |
@@ -79,11 +79,9 @@ ASR 的独立证据 sidecar,不改变 `asr_result.json` 的既有数组结构
79
79
  ```
80
80
 
81
81
  `status` 可为 `AVAILABLE_COARSE`、`EXPLICITLY_SKIPPED`、`UNAVAILABLE_NO_KEY`、
82
- `UNAVAILABLE_NO_DURATION`、`FAILED_AUDIO_EXTRACTION`、`FAILED_PROVIDER`、`EMPTY_UNKNOWN`
83
- 或 `LEGACY_UNVERIFIED`。`LEGACY_UNVERIFIED` 标记没有旧 sidecar 的兼容缓存,可离线复用但
84
- `observed_text`/`glossary_modified` 为 `null`,且始终保持 legacy 身份;非 legacy sidecar 记录
85
- 当时参与修正的人名/别名列表(`glossary.names`),人名表变化或所描述的文件被重写(size/mtime 不再
86
- 一致)都会使 ASR 缓存失效。
82
+ `UNAVAILABLE_NO_DURATION`、`FAILED_AUDIO_EXTRACTION`、`FAILED_PROVIDER` 或 `EMPTY_UNKNOWN`。
83
+ sidecar 记录当时参与修正的人名/别名列表(`glossary.names`),人名表变化、所描述的文件被重写
84
+ (size/mtime 不再一致)或 sidecar 缺失,都会使 ASR 缓存失效并重跑 ASR。
87
85
  `UNAVAILABLE_NO_DURATION` 与 `EMPTY_UNKNOWN` 是可重试的不可用结果,不作为缓存命中;写作
88
86
  brief 会校验 sidecar 并打印当前状态,缺失或与当前文件不一致时显示 `MISSING_OR_STALE`。
89
87
 
@@ -143,22 +141,7 @@ brief 会校验 sidecar 并打印当前状态,缺失或与当前文件不一
143
141
  ]
144
142
  ```
145
143
 
146
- ## deslop_qc_requirements.json(工具/brief 生成的运行契约)
147
-
148
- `deslop_qc_requirements.json` 是 tool/brief generated run contract:工具或 brief 生成本次运行的 QC 要求,供 `deslop_qc` 读取,不由 Agent 手写。字段为 `schema_version` 与 `style_card_required`。
149
-
150
- `style_card_required` 默认 `false`(advisory):缺少 `style_card.json` 只是 warning,不阻断出片。将来的 opt-in 运行可把它设为 `true`,让 `style_card.json` 成为硬性要求——`deslop_qc` 只读这个字段判断缺少 `style_card.json` 是否是 blocker,不扫描 `agent_narration_brief.md` 的 prompt wording 来推断。如果 requirements 文件缺失或损坏,按 legacy/migration advisory 处理,不作为 hard failure。
151
-
152
- 该契约不改变 `--style`:`--style` 仍是 freeform verbatim guidance,不增加固定风格档位。它也不改变 `deslop_qc` 边界:仍然是 report-only,不是 AIGC detector,不自动改写。
153
-
154
- 最小示例:
155
-
156
- ```json
157
- {
158
- "schema_version": 1,
159
- "style_card_required": false
160
- }
161
- ```
144
+ cut 第二轮(已有 `edited_source.mp4`)时本文件与 `asr_writing_chunks.json` 都在 OUTPUT 时间轴上:被拆到多个片段的场景 `scene_id` 写作 `"3.1"`(0 起的原场景号加片段序号);VLM 文字里落在场景原片区间内的时间改写成输出时间,落在被剪掉部分的写 `[cut-away moment]`;只被剪进一部分的 ASR 窗口文字换成 `[partial ASR window: only part of it is in the cut, text withheld]`。`scene_id` 一律 0 起,brief 里给人看的场景号从 1 数。
162
145
 
163
146
  ## background_research.json
164
147
 
@@ -190,4 +173,4 @@ brief 会校验 sidecar 并打印当前状态,缺失或与当前文件不一
190
173
 
191
174
  ## 其他产物
192
175
 
193
- `narration.json`、`narration_lint.json`、`style_card.json`、`packaging_plan.json`、`deslop_qc.json`、`clip_plan.json`、`clip_plan_validated.json` 由后续的写稿与剪辑阶段读写,本技能既不生成也不校验它们;其格式以本技能生成的 `agent_narration_brief.md` 和编排器的中间产物契约为准。
176
+ `narration.json`、`narration_lint.json`、`style_card.json`、`packaging_plan.json`、`clip_plan.json`、`clip_plan_validated.json` 由后续的写稿与剪辑阶段读写,本技能既不生成也不校验它们;其格式以本技能生成的 `agent_narration_brief.md` 和编排器的中间产物契约为准。
@@ -1,10 +1,30 @@
1
- """Normalize, split, budget, and de-duplicate narration text."""
1
+ """Split, chunk, and budget text for the agent narration brief."""
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
5
+ from lib import CONFIG
6
+
7
+
8
+ def _sentence_pieces(text: str) -> list[str]:
9
+ """Split text into sentence-like pieces while keeping terminal punctuation."""
10
+ text = re.sub(r"\s+", " ", text).strip()
11
+ if not text:
12
+ return []
13
+ parts = re.split(r"([。!?!?;;.])", text)
14
+ out: list[str] = []
15
+ for idx in range(0, len(parts), 2):
16
+ body = parts[idx].strip()
17
+ punct = parts[idx + 1] if idx + 1 < len(parts) else ""
18
+ if body or punct:
19
+ out.append((body + punct).strip())
20
+ return out or [text]
21
+
22
+
23
+ def _text_units(text: str) -> int:
24
+ """Length unit for prose: CJK characters, otherwise words."""
25
+ if re.search(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]", text):
26
+ return len(re.sub(r"\s+", "", text))
27
+ return len(re.findall(r"\b\w+\b", text))
8
28
 
9
29
 
10
30
  def _format_frame_facts(scene):
@@ -16,17 +36,6 @@ def _format_frame_facts(scene):
16
36
  return "\n 帧动作:\n" + "\n".join(lines)
17
37
 
18
38
 
19
- def _text_char_count(text):
20
- """计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
21
- return len(
22
- re.sub(
23
- r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]',
24
- "",
25
- text,
26
- )
27
- )
28
-
29
-
30
39
  def _split_text_by_sentence_windows(text, min_chars=500, max_chars=800):
31
40
  """Clipto-style three-tier sentence boundary splitting for long ASR text."""
32
41
  text = re.sub(r"\s+", " ", text).strip()
@@ -177,66 +186,6 @@ def _chunk_asr_for_writing(segments, scenes_analysis, min_chars=None, max_chars=
177
186
  return chunks
178
187
 
179
188
 
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
238
-
239
-
240
189
  def _scene_available_seconds(start, end):
241
190
  return max(0.0, end - start - CONFIG["narration_tail_pad_seconds"])
242
191
 
@@ -251,51 +200,5 @@ def _recommended_char_budget(start, end):
251
200
  return int(_scene_available_seconds(start, end) * effective_rate)
252
201
 
253
202
 
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)
298
-
299
-
300
203
  def _overlap_seconds(start, end, other_start, other_end):
301
204
  return max(0.0, min(end, other_end) - max(start, other_start))
@@ -6,7 +6,7 @@ import time
6
6
  from pathlib import Path
7
7
 
8
8
  from lib import CONFIG
9
- from lib import log, run_cmd, get_video_duration, mimo_asr_api_call
9
+ from lib import log, run_cmd, get_video_duration, mimo_asr_api_call, is_moderation_refusal
10
10
  from detect import _audio_meta_path, _write_audio_meta
11
11
  from asr_timing_evidence import (
12
12
  EVIDENCE_FILENAME,
@@ -202,13 +202,13 @@ def _run_asr(wav_path):
202
202
 
203
203
  音频以 base64 data-URI 放进 OpenAI 风格的 chat/completions 消息里,转写文本回到
204
204
  choices[0].message.content。API/响应结构失败会抛错,避免把瞬时失败缓存成空转写;
205
- 只有无音频、超体积等确定不可发送的片段返回空串。
205
+ 音频文件缺失/不可读同样抛错(那是上游 bug,不是静音);只有空音频、超体积等
206
+ 确定不可发送的片段返回空串。
206
207
  """
207
208
  try:
208
209
  raw = Path(wav_path).read_bytes()
209
210
  except OSError as e:
210
- log(f"ASR 警告: 无法读取音频 {wav_path}: {e}")
211
- return ""
211
+ raise RuntimeError(f"ASR: 无法读取音频 {wav_path}: {e}") from e
212
212
  if not raw:
213
213
  return ""
214
214
 
@@ -235,11 +235,17 @@ def _run_asr(wav_path):
235
235
  except Exception as e:
236
236
  raise ASRProviderError(f"MiMo ASR 调用失败: {e}") from e
237
237
  try:
238
- return _strip_reasoning_residue(str(resp["choices"][0]["message"]["content"] or "")).strip()
238
+ text = _strip_reasoning_residue(str(resp["choices"][0]["message"]["content"] or "")).strip()
239
239
  except (KeyError, IndexError, TypeError):
240
240
  raise ASRProviderError(
241
241
  f"MiMo ASR 返回结构异常: {json.dumps(resp, ensure_ascii=False)[:200]}"
242
242
  )
243
+ if is_moderation_refusal(text):
244
+ # The refusal sentence is not dialogue: storing it would put it in the briefs and
245
+ # make cut/narration safety treat the window as speech. Record no text instead.
246
+ log(f"ASR 警告: MiMo 内容审核拒绝转写 {Path(wav_path).name},该段记为无文本")
247
+ return ""
248
+ return text
243
249
 
244
250
 
245
251
  def _segment_and_transcribe(audio_wav, segments_dir, total_duration, segment_length=None):