@oh-story/dsh 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/README.md +17 -11
  2. package/lib/client.js +85 -35
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +41 -36
  5. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
  6. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
  7. package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
  8. package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
  9. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
  10. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
  11. package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
  12. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
  13. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
  14. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
  15. package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
  16. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
  17. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
  18. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
  19. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
  20. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  21. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
  22. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
  23. package/lib/index.js +16 -7
  24. package/lib/index.js.map +2 -2
  25. package/lib/novel-to-game/README.md +98 -44
  26. package/lib/novel-to-game/README_ZH.md +100 -46
  27. package/lib/novel-to-game/VERSION +1 -1
  28. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
  29. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
  30. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
  31. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
  32. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
  33. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
  34. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
  35. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
  36. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
  37. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
  38. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
  39. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
  40. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
  41. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
  42. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
  43. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
  44. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
  45. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
  46. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
  47. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
  57. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
  58. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
  59. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
  60. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
  61. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
  62. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
  63. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
  64. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
  65. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
  66. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
  91. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
  92. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
  93. package/lib/novel-to-game/manifest.json +303 -78
  94. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
  95. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
  96. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
  98. package/lib/types/client/drama-production-view.d.ts +11 -0
  99. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  100. package/lib/types/client/file-activity.d.ts +1 -1
  101. package/lib/types/client/file-activity.d.ts.map +1 -1
  102. package/lib/types/client/production-prompts.d.ts +7 -1
  103. package/lib/types/client/production-prompts.d.ts.map +1 -1
  104. package/lib/types/client/production-runtime.d.ts +7 -2
  105. package/lib/types/client/production-runtime.d.ts.map +1 -1
  106. package/lib/types/drama-adapters.d.ts.map +1 -1
  107. package/lib/types/skill-provider.d.ts.map +1 -1
  108. package/lib/types/video-project.d.ts.map +1 -1
  109. package/lib/video-recap/CHANGELOG.md +108 -1
  110. package/lib/video-recap/README.en.md +18 -14
  111. package/lib/video-recap/README.md +18 -14
  112. package/lib/video-recap/manifest.json +297 -321
  113. package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
  114. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
  115. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
  116. package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
  117. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
  118. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
  119. package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
  120. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
  121. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
  122. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
  123. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
  124. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
  125. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
  126. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
  127. package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
  128. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
  129. package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
  130. package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
  131. package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
  132. package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
  133. package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
  134. package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
  135. package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
  136. package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
  137. package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
  138. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
  139. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
  140. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
  141. package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
  142. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
  143. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
  144. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
  145. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
  146. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
  147. package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
  148. package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
  149. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
  150. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
  151. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
  152. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
  153. package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
  154. package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
  155. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
  156. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
  157. package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
  158. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
  159. package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
  160. package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
  161. package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
  162. package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
  163. package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
  164. package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
  165. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
  166. package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
  167. package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
  168. package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
  169. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
  170. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
  171. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
  172. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
  173. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
  174. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
  175. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
  176. package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
  177. package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
  178. package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
  179. package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
  180. package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
  181. package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
  182. package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
  183. package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
  184. package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
  185. package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
  186. package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
  187. package/lib/video-recap/skills/video-script/SKILL.md +9 -7
  188. package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
  189. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
  190. package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
  191. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
  192. package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
  193. package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
  194. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
  195. package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
  196. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
  197. package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
  198. package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
  199. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
  200. package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
  201. package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
  202. package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
  203. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
  204. package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
  205. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
  206. package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
  207. package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
  208. package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
  209. package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
  210. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
  211. package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
  212. package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
  213. package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
  214. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
  215. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
  216. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
  217. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
  218. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
  219. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
  220. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
  221. package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
  222. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
  223. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
  224. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
  225. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
  226. package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
  227. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
  228. package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
  229. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
  230. package/package.json +45 -45
  231. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  232. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  233. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  234. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
  235. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
  236. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
  237. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
  238. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
  239. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
  240. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
  241. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
  242. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
  243. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
  244. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
  245. package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
  246. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
  247. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
  248. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
  249. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
  250. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
  251. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
  252. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
  253. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
  254. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
  255. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
  256. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
  257. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
  258. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
  259. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
  260. package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
  262. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
  263. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
  264. /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
@@ -5,7 +5,16 @@ import re
5
5
  import subprocess
6
6
  from pathlib import Path
7
7
 
8
+ from frame_grid import edge_frame_snapper, snap_edges_to_frames
8
9
  from lib import log
10
+ from sentence_gate import (
11
+ _GATE_TOLERANCE,
12
+ _continuous_source_join,
13
+ _edge_classifier,
14
+ _edge_joined,
15
+ _unverified_window,
16
+ enforce_clip_sentence_boundaries,
17
+ )
9
18
 
10
19
 
11
20
  def _find_source_artifact(work_dir, filename, source_id=None, source_work_dir=None):
@@ -20,11 +29,12 @@ def _find_source_artifact(work_dir, filename, source_id=None, source_work_dir=No
20
29
 
21
30
 
22
31
  def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=None):
23
- """Load reliable sentence-end pause windows produced by video-understanding.
32
+ """Load usable sentence-end pause windows produced by video-understanding.
24
33
 
25
34
  A sentence anchor's `time` is the acoustic pause end, while `pause_start` is already
26
- after the final spoken sample. Any cut within that closed interval preserves the sentence.
27
- Low-confidence anchors are deliberately excluded from the hard-safety path.
35
+ after the final spoken sample. Any cut within that closed interval is word-safe.
36
+ `boundary_use: unverified` anchors (coarse-ASR estimates) stay usable but keep that
37
+ label so an edge resting on one is reported as such; `none` anchors are excluded.
28
38
  """
29
39
  path = _find_source_artifact(
30
40
  work_dir, "speech_boundary_anchors.json", source_id, source_work_dir
@@ -32,35 +42,80 @@ def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=No
32
42
  if path is None:
33
43
  return []
34
44
  payload = json.loads(path.read_text(encoding="utf-8"))
35
- windows = [
36
- {
45
+ windows = []
46
+ for anchor in payload["sentence_anchors"]:
47
+ # Schema-1 anchors (no `boundary_use`) came from the old coarse estimator: high/medium
48
+ # labels there are usable but unverified.
49
+ use = anchor.get("boundary_use") or (
50
+ "unverified" if anchor["confidence"] in {"high", "medium"} else "none"
51
+ )
52
+ if use == "none":
53
+ continue
54
+ windows.append({
37
55
  "start": round(anchor["pause_start"], 3),
38
56
  "end": round(anchor["time"], 3),
39
57
  "kind": "sentence_anchor",
40
58
  "confidence": anchor["confidence"],
41
- }
42
- for anchor in payload["sentence_anchors"]
43
- if anchor["confidence"] in {"high", "medium"}
44
- ]
59
+ "boundary_use": use,
60
+ })
45
61
  return sorted(windows, key=lambda row: (row["start"], row["end"]))
46
62
 
47
63
 
48
- def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
49
- """Merged ASR speech spans (asr_clean.json wins over asr_result.json).
64
+ # Interjections and common ASR artifacts on screams/music. A window whose text is only these
65
+ # is not dialogue at a clip edge or narration entry; real short lines such as "救我!" still are.
66
+ # Same copy in video-cut, video-script and video-assemble (parity-tested by function).
67
+ _NON_DIALOGUE_TOKENS = frozenset(
68
+ "啊 嗯 哼 哦 呃 唉 嘿 呦 哈 呀 hi yeah ok okay oh uh ah hmm".split()
69
+ )
70
+ _NON_DIALOGUE_CJK = frozenset("啊嗯哼哦呃唉嘿呦哈呀")
71
+ # Lines cross 15s ASR window edges (a line may run 13.2–15.4 while its window ends at 15.0),
72
+ # so an interjection-only window next to real dialogue keeps this much of its shared edge.
73
+ _INTERJECTION_GUARD_SECONDS = 1.0
50
74
 
51
- Only used to decide whether an unsafe edge blocks; a missing transcript means unchecked.
75
+
76
+ def _interjection_only(text):
77
+ tokens = [token for token in re.split(r"[\W_]+", text.lower()) if token]
78
+ # Punctuation-only rows ("……", "?") are often ASR for unintelligible speech: keep them.
79
+ return bool(tokens) and all(
80
+ token in _NON_DIALOGUE_TOKENS or set(token) <= _NON_DIALOGUE_CJK for token in tokens
81
+ )
82
+
83
+
84
+ def _dialogue_speech_spans(rows):
85
+ """Merged dialogue spans from timed ASR rows.
86
+
87
+ A row holding only interjections ("啊!", "Hi.") is not dialogue, except a
88
+ `_INTERJECTION_GUARD_SECONDS` guard on an edge it shares with a dialogue row. A row
89
+ whose text is empty or whitespace (ASR heard no words) is timing-only evidence: it is
90
+ skipped here and guards nothing. A row with no `text` field is measured timing whose
91
+ words are unknown and counts as dialogue.
52
92
  """
53
- rows = []
54
- for filename in ("asr_clean.json", "asr_result.json"):
55
- path = _find_source_artifact(work_dir, filename, source_id, source_work_dir)
56
- if path is not None:
57
- payload = json.loads(path.read_text(encoding="utf-8"))
58
- rows = payload["segments"] if filename == "asr_clean.json" else payload
59
- break
60
- spans = sorted(
61
- ({"start": row["start"], "end": row["end"]} for row in rows if row["text"].strip()),
93
+ rows = sorted(
94
+ (
95
+ {
96
+ "start": row["start"],
97
+ "end": row["end"],
98
+ "dialogue": not _interjection_only(row.get("text", "")),
99
+ }
100
+ for row in rows
101
+ if "text" not in row or row["text"].strip()
102
+ ),
62
103
  key=lambda row: (row["start"], row["end"]),
63
104
  )
105
+ spans = []
106
+ for idx, row in enumerate(rows):
107
+ if row["dialogue"]:
108
+ spans.append({"start": row["start"], "end": row["end"]})
109
+ continue
110
+ before = rows[idx - 1] if idx > 0 else None
111
+ after = rows[idx + 1] if idx + 1 < len(rows) else None
112
+ if before and before["dialogue"] and row["start"] - before["end"] <= 0.05:
113
+ end = min(row["end"], row["start"] + _INTERJECTION_GUARD_SECONDS)
114
+ spans.append({"start": row["start"], "end": end})
115
+ if after and after["dialogue"] and after["start"] - row["end"] <= 0.05:
116
+ start = max(row["start"], row["end"] - _INTERJECTION_GUARD_SECONDS)
117
+ spans.append({"start": start, "end": row["end"]})
118
+ spans.sort(key=lambda row: (row["start"], row["end"]))
64
119
  merged = []
65
120
  for span in spans:
66
121
  if merged and span["start"] <= merged[-1]["end"] + 0.05:
@@ -70,98 +125,36 @@ def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
70
125
  return merged
71
126
 
72
127
 
73
- def _combine_boundary_windows(*groups):
74
- unique = {
75
- (round(row["start"], 3), round(row["end"], 3)) for group in groups for row in group
76
- }
77
- return [{"start": start, "end": end} for start, end in sorted(unique)]
78
-
79
-
80
- def _same_source(left, right):
81
- # Single-source normalized plans omit source identity; both sides then read as None.
82
- return left.get("source_id") == right.get("source_id") and left.get(
83
- "source_path"
84
- ) == right.get("source_path")
85
-
86
-
87
- def _continuous_source_join(left, right, tolerance=0.05):
88
- return (
89
- _same_source(left, right)
90
- and abs(left["source_end"] - right["source_start"]) <= tolerance
91
- and abs(left["output_end"] - right["output_start"]) <= tolerance
92
- )
93
-
94
-
95
- def enforce_clip_sentence_boundaries(
96
- plan, boundary_windows, speech_spans, video_duration, tolerance=0.05
97
- ):
98
- """Block any audible clip edge that falls inside detected source speech.
128
+ def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
129
+ """Merged ASR dialogue spans (asr_clean.json wins over asr_result.json).
99
130
 
100
- Safe edges are: source start/end, a reliable sentence/quiet pause, or a truly contiguous
101
- same-source join (no media is removed). Missing ASR timing degrades to `unchecked` rather
102
- than inventing speech. Once ASR says an edge is speech-owned, failure to snap is blocking.
131
+ Only used to decide whether an unsafe edge blocks; a missing transcript means unchecked.
103
132
  """
104
- clips = plan["clips"]
105
- checks, new_blockers = [], []
106
-
107
- def inside(rows, ts):
108
- return any(row["start"] - tolerance <= ts <= row["end"] + tolerance for row in rows)
109
-
110
- for idx, clip in enumerate(clips):
111
- for edge, ts in (("start", clip["source_start"]), ("end", clip["source_end"])):
112
- contiguous = (
113
- edge == "start"
114
- and idx > 0
115
- and _continuous_source_join(clips[idx - 1], clip, tolerance)
116
- ) or (
117
- edge == "end"
118
- and idx + 1 < len(clips)
119
- and _continuous_source_join(clip, clips[idx + 1], tolerance)
120
- )
121
- if edge == "start" and ts <= tolerance:
122
- status, reason = "safe", "source_start"
123
- elif edge == "end" and ts >= video_duration - tolerance:
124
- status, reason = "safe", "source_end"
125
- elif contiguous:
126
- status, reason = "safe", "continuous_source_join"
127
- elif inside(boundary_windows, ts):
128
- status, reason = "safe", "sentence_or_quiet_boundary"
129
- elif not speech_spans:
130
- status, reason = "unchecked", "speech_timing_unavailable"
131
- elif not inside(speech_spans, ts):
132
- status, reason = "safe", "outside_detected_speech"
133
- else:
134
- status, reason = "blocking", "inside_detected_speech"
135
- check = {
136
- "clip_id": clip["clip_id"],
137
- "source_id": clip.get("source_id"),
138
- "edge": edge,
139
- "time": round(ts, 3),
140
- "status": status,
141
- "reason": reason,
142
- }
143
- checks.append(check)
144
- if status == "blocking":
145
- new_blockers.append(
146
- {
147
- "code": "unsafe_clip_sentence_boundary",
148
- **check,
149
- "message": "剪辑边界仍落在原声讲话区间内,必须移动到句末锚点,不能截断原声句子。",
150
- }
151
- )
133
+ rows = []
134
+ for filename in ("asr_clean.json", "asr_result.json"):
135
+ path = _find_source_artifact(work_dir, filename, source_id, source_work_dir)
136
+ if path is not None:
137
+ payload = json.loads(path.read_text(encoding="utf-8"))
138
+ rows = payload["segments"] if filename == "asr_clean.json" else payload
139
+ break
140
+ return _dialogue_speech_spans(row for row in rows if row["text"].strip())
152
141
 
153
- qc = plan.setdefault("qc", {})
154
- qc.setdefault("boundary_status", {})["sentence_checks"] = checks
155
- existing = [
156
- row
157
- for row in qc.get("blocking", [])
158
- if row["code"] != "unsafe_clip_sentence_boundary"
159
- ]
160
- if existing or new_blockers:
161
- qc["blocking"] = existing + new_blockers
162
- else:
163
- qc.pop("blocking", None)
164
- return plan
142
+
143
+ def _combine_boundary_windows(*groups):
144
+ """Dedupe windows by span; a span backed by any verified/quiet row counts as verified."""
145
+ unique = {}
146
+ for group in groups:
147
+ for row in group:
148
+ key = (round(row["start"], 3), round(row["end"], 3))
149
+ if key not in unique or _unverified_window(unique[key]):
150
+ unique[key] = row
151
+ combined = []
152
+ for (start, end), row in sorted(unique.items()):
153
+ item = {"start": start, "end": end, "kind": row.get("kind", "quiet_window")}
154
+ if _unverified_window(row):
155
+ item["boundary_use"] = "unverified"
156
+ combined.append(item)
157
+ return combined
165
158
 
166
159
 
167
160
  def _recompute_clip_timeline(clips):
@@ -420,7 +413,12 @@ def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
420
413
  return sorted(changes)
421
414
 
422
415
 
423
- def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
416
+ # How far past each edge's own search window the shot-change scan also looks, so a hard cut
417
+ # within a frame on the other side of the edge is known to the frame-grid pass.
418
+ _FRAME_CUT_SCAN_PAD = 0.1
419
+
420
+
421
+ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5, found_cuts=None):
424
422
  """Nudge each clip's boundaries clear of the ORIGINAL footage's hard cuts to avoid 闪烁.
425
423
 
426
424
  A clip whose source_start sits just before a shot-change opens on a brief sliver of the old
@@ -430,9 +428,14 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
430
428
  - move source_end BACK onto a shot-change in [end-margin, end) → clean close
431
429
  Boundaries already on a cut, or with no nearby cut, are left untouched. Snaps that would shrink
432
430
  a clip below `min_keep` are skipped. Recomputes the output timeline cursor-based.
431
+
432
+ `found_cuts` (a set) collects every cut the scan saw, including those up to
433
+ `_FRAME_CUT_SCAN_PAD` on the far side of each edge, for the frame-grid pass's tie-break.
433
434
  """
434
435
  if margin <= 0:
435
436
  return plan
437
+ pad = _FRAME_CUT_SCAN_PAD if found_cuts is not None else 0.0
438
+ found_cuts = set() if found_cuts is None else found_cuts
436
439
  clips = [dict(c) for c in plan["clips"]]
437
440
  n_start = n_end = 0
438
441
  events = []
@@ -449,11 +452,9 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
449
452
  "end_action": "kept",
450
453
  }
451
454
  # Opening: a shot-change just AFTER source_start leaves an old-shot sliver before it.
452
- start_changes = [
453
- c
454
- for c in _detect_shot_changes(video, s, min(e, s + margin), threshold)
455
- if c > s + 1e-3
456
- ]
455
+ seen = _detect_shot_changes(video, max(0.0, s - pad), min(e, s + margin), threshold)
456
+ found_cuts.update(seen)
457
+ start_changes = [c for c in seen if c > s + 1e-3]
457
458
  if start_changes:
458
459
  cand = max(start_changes) # open after the last rapid cut in the window
459
460
  if cand < e - min_keep:
@@ -463,11 +464,9 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
463
464
  else:
464
465
  event["start_unsnapped_reason"] = "collapse"
465
466
  # Closing: a shot-change just BEFORE source_end leaves a next-shot sliver after it.
466
- end_changes = [
467
- c
468
- for c in _detect_shot_changes(video, max(new_s, e - margin), e, threshold)
469
- if c < e - 1e-3
470
- ]
467
+ seen = _detect_shot_changes(video, max(new_s, e - margin), e + pad, threshold)
468
+ found_cuts.update(seen)
469
+ end_changes = [c for c in seen if c < e - 1e-3]
471
470
  if end_changes:
472
471
  cand = min(end_changes) # close before the first rapid cut in the window
473
472
  if cand > new_s + min_keep:
@@ -489,15 +488,57 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
489
488
  return _plan_with_snapped_clips(plan, clips, "shot_snaps", events)
490
489
 
491
490
 
491
+ def _revert_unsafe_shot_snaps(plan, before, classify):
492
+ """Undo a shot-change move that left an edge in speech where the plan's own edge was safe.
493
+
494
+ Sound outranks picture cleanup. Line snapping can pull a moved edge back out of speech
495
+ only next to a pause window, so an end the shot-change pass pulled back up to
496
+ `scene_margin` into a sentence (the plan ended it in an ASR gap that is music, not
497
+ silence) would block, and the nearest safe edge the gate suggests would be pulled back
498
+ again on the next run. Such an edge returns to `before` (the clips before the shot-change
499
+ pass) and its shot_snaps event reads `reverted_unsafe`, unless that would overlap
500
+ another clip's source range.
501
+ """
502
+ events = plan.get("qc", {}).get("boundary_status", {}).get("shot_snaps", [])
503
+ if len(events) != len(before):
504
+ return plan
505
+ clips = [dict(c) for c in plan["clips"]]
506
+ events = [dict(e) for e in events]
507
+ reverted = 0
508
+ for idx, (clip, old, event) in enumerate(zip(clips, before, events)):
509
+ for edge, key, moved in (("start", "source_start", "moved_forward"),
510
+ ("end", "source_end", "moved_back")):
511
+ if event[f"{edge}_action"] != moved:
512
+ continue
513
+ status, reason = classify(edge, clip[key], _edge_joined(clips, idx, edge))
514
+ if status != "blocking" or classify(
515
+ edge, old[key], _edge_joined(before, idx, edge))[0] != "safe":
516
+ continue
517
+ span = {**clip, key: old[key]}
518
+ if not plan["allow_overlap"] and _candidate_overlaps(
519
+ clips, idx, span["source_start"], span["source_end"]):
520
+ continue
521
+ clip[key] = old[key]
522
+ event[f"{edge}_action"] = "reverted_unsafe"
523
+ event[f"rejected_{edge}"] = event.pop(f"new_{edge}")
524
+ event[f"{edge}_revert_reason"] = reason
525
+ reverted += 1
526
+ if not reverted:
527
+ return plan
528
+ log(f"切镜头避让会截断原声,已撤回 {reverted} 处边界")
529
+ return _plan_with_snapped_clips(plan, clips, "shot_snaps", events)
530
+
531
+
492
532
  def _load_silence_for_source(work_dir, source_id, source_work_dir=None):
493
533
  """Read a source's silence_periods.json (project layout when source_id is None); [] when absent."""
494
534
  path = _find_source_artifact(work_dir, "silence_periods.json", source_id, source_work_dir)
495
535
  return json.loads(path.read_text(encoding="utf-8")) if path else []
496
536
 
497
537
 
498
- def snap_multi_source_clips(
538
+ def snap_source_clips(
499
539
  plan,
500
- sources,
540
+ video,
541
+ duration,
501
542
  work_dir,
502
543
  *,
503
544
  line_max_extend,
@@ -507,13 +548,68 @@ def snap_multi_source_clips(
507
548
  start_max_trim,
508
549
  do_line_snap=True,
509
550
  do_scene_snap=True,
551
+ source_id=None,
552
+ source_work_dir=None,
553
+ frame_grid=None,
510
554
  ):
511
- """Per-source line/shot snapping for a multi-source validated plan.
555
+ """Snap every clip of ONE source: shot changes, quiet starts/ends, frame grid, then the gate.
556
+
557
+ Visual cleanup goes first. Sentence/quiet snapping is the sound authority because a
558
+ clean picture is never allowed to reintroduce a mid-sentence audio cut (a shot-change
559
+ move line snapping cannot repair is reverted when the plan's own edge was gate-safe);
560
+ the frame-grid pass only moves an edge by under a frame and prefers the side that keeps
561
+ it safe, then the side that does not straddle a hard cut the shot-change scan saw, and
562
+ the gate judges the final, frame-aligned edges. source_id None reads the
563
+ project-level understanding artifacts (single-source layout). frame_grid is a
564
+ frame_grid.source_frame_grids() entry; None leaves edges off the grid.
565
+ """
566
+ before_shot_snap = [dict(c) for c in plan["clips"]]
567
+ scene_cuts = set()
568
+ if do_scene_snap:
569
+ plan = snap_clips_off_shot_changes(
570
+ plan, video, margin=scene_margin, threshold=scene_threshold, found_cuts=scene_cuts
571
+ )
572
+ boundaries = _combine_boundary_windows(
573
+ _load_silence_for_source(work_dir, source_id, source_work_dir),
574
+ _load_sentence_boundary_windows(work_dir, source_id, source_work_dir),
575
+ )
576
+ if do_line_snap:
577
+ plan = snap_clip_starts_to_lines(
578
+ plan, boundaries, duration, start_max_prepend, max_trim=start_max_trim
579
+ )
580
+ plan = snap_clip_ends_to_lines(plan, boundaries, duration, line_max_extend)
581
+ speech_spans = _load_source_speech_spans(work_dir, source_id, source_work_dir)
582
+ video_start = frame_grid["origin"] if frame_grid is not None else 0.0
583
+ classify = _edge_classifier(boundaries, speech_spans, duration, _GATE_TOLERANCE,
584
+ video_start)
585
+ if do_scene_snap:
586
+ plan = _revert_unsafe_shot_snaps(plan, before_shot_snap, classify)
587
+ if frame_grid is None:
588
+ return enforce_clip_sentence_boundaries(plan, boundaries, speech_spans, duration)
589
+
590
+ frame_grid = {**frame_grid, "scene_cuts": sorted(scene_cuts)}
591
+ clips = plan["clips"]
592
+ joined = [idx > 0 and _continuous_source_join(clips[idx - 1], clip)
593
+ for idx, clip in enumerate(clips)]
594
+ clips, events = snap_edges_to_frames(
595
+ clips, frame_grid, boundaries, classify, duration,
596
+ allow_overlap=plan["allow_overlap"], joined_to_previous=joined,
597
+ )
598
+ plan = _plan_with_snapped_clips(plan, clips, "frame_snaps", events)
599
+ return enforce_clip_sentence_boundaries(
600
+ plan, boundaries, speech_spans, duration, video_start=video_start,
601
+ frame_snap=edge_frame_snapper(frame_grid, boundaries, classify, duration),
602
+ )
603
+
512
604
 
513
- Each clip is snapped using ITS OWN source's silence windows / shot changes and duration
514
- (a clip in source B never constrains a clip in source A), then the global OUTPUT timeline
515
- is recomputed once in plan order. Missing silence data leaves a boundary unchanged.
516
- Mirrors the single-source snap_clip_ends_to_lines + snap_clips_off_shot_changes.
605
+ def snap_multi_source_clips(plan, sources, work_dir, frame_grids=None, **snap_options):
606
+ """Per-source snap_source_clips for a multi-source validated plan.
607
+
608
+ Each clip is snapped using ITS OWN source's silence windows / shot changes, frame grid
609
+ and duration (a clip in source B never constrains a clip in source A), then the global
610
+ OUTPUT timeline is recomputed once in plan order. Missing silence data leaves a boundary
611
+ unchanged. `frame_grids` maps source_id to its frame grid; `snap_options` are
612
+ snap_source_clips' keyword options.
517
613
  """
518
614
  clips = plan["clips"]
519
615
  allow_overlap = plan["allow_overlap"]
@@ -524,34 +620,21 @@ def snap_multi_source_clips(
524
620
  "start_snaps": [],
525
621
  "end_snaps": [],
526
622
  "shot_snaps": [],
623
+ "frame_snaps": [],
527
624
  "sentence_checks": [],
528
625
  }
529
626
  blocking_accum = []
530
627
  for sid, group in groups.items():
531
628
  source = sources[sid]
532
- duration = source["duration"]
533
- mini = {"clips": [dict(c) for c in group], "allow_overlap": allow_overlap}
534
- # Visual cleanup goes first. Sentence/quiet snapping is the final authority because
535
- # a clean picture is never allowed to reintroduce a mid-sentence audio cut.
536
- if do_scene_snap:
537
- mini = snap_clips_off_shot_changes(
538
- mini, source["source_path"], margin=scene_margin, threshold=scene_threshold
539
- )
540
- source_work_dir = source.get("source_work_dir")
541
- boundaries = _combine_boundary_windows(
542
- _load_silence_for_source(work_dir, sid, source_work_dir),
543
- _load_sentence_boundary_windows(work_dir, sid, source_work_dir),
544
- )
545
- if do_line_snap:
546
- mini = snap_clip_starts_to_lines(
547
- mini, boundaries, duration, start_max_prepend, max_trim=start_max_trim
548
- )
549
- mini = snap_clip_ends_to_lines(mini, boundaries, duration, line_max_extend)
550
- mini = enforce_clip_sentence_boundaries(
551
- mini,
552
- boundaries,
553
- _load_source_speech_spans(work_dir, sid, source_work_dir),
554
- duration,
629
+ mini = snap_source_clips(
630
+ {"clips": [dict(c) for c in group], "allow_overlap": allow_overlap},
631
+ source["source_path"],
632
+ source["duration"],
633
+ work_dir,
634
+ source_id=sid,
635
+ source_work_dir=source.get("source_work_dir"),
636
+ frame_grid=(frame_grids or {}).get(sid),
637
+ **snap_options,
555
638
  )
556
639
  mini_boundary = mini["qc"]["boundary_status"]
557
640
  for key, events in boundary_accum.items():