@oh-story/dsh 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +18 -12
  2. package/lib/client.js +1003 -652
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +41 -36
  5. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
  6. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
  7. package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
  8. package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
  9. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
  10. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
  11. package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
  12. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
  13. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
  14. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
  15. package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
  16. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
  17. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
  18. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
  19. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
  20. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  21. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
  22. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
  23. package/lib/index.js +17 -7
  24. package/lib/index.js.map +2 -2
  25. package/lib/novel-to-game/README.md +98 -44
  26. package/lib/novel-to-game/README_ZH.md +100 -46
  27. package/lib/novel-to-game/VERSION +1 -1
  28. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
  29. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
  30. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
  31. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
  32. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
  33. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
  34. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
  35. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
  36. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
  37. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
  38. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
  39. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
  40. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
  41. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
  42. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
  43. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
  44. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
  45. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
  46. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
  47. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
  48. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
  49. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
  50. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
  51. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
  52. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
  53. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
  54. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
  55. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
  56. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
  57. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
  58. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
  59. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
  60. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
  61. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
  62. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
  63. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
  64. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
  65. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
  66. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
  91. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
  92. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
  93. package/lib/novel-to-game/manifest.json +303 -78
  94. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
  95. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
  96. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
  97. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
  98. package/lib/types/client/canvas-viewport.d.ts +31 -0
  99. package/lib/types/client/canvas-viewport.d.ts.map +1 -0
  100. package/lib/types/client/drama-production-view.d.ts +14 -2
  101. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  102. package/lib/types/client/file-activity.d.ts +1 -1
  103. package/lib/types/client/file-activity.d.ts.map +1 -1
  104. package/lib/types/client/index.d.ts.map +1 -1
  105. package/lib/types/client/production-canvas.d.ts +24 -0
  106. package/lib/types/client/production-canvas.d.ts.map +1 -0
  107. package/lib/types/client/production-prompts.d.ts +7 -1
  108. package/lib/types/client/production-prompts.d.ts.map +1 -1
  109. package/lib/types/client/production-runtime.d.ts +7 -2
  110. package/lib/types/client/production-runtime.d.ts.map +1 -1
  111. package/lib/types/drama-adapters.d.ts.map +1 -1
  112. package/lib/types/skill-provider.d.ts.map +1 -1
  113. package/lib/types/video-project.d.ts.map +1 -1
  114. package/lib/video-recap/CHANGELOG.md +123 -1
  115. package/lib/video-recap/README.en.md +20 -16
  116. package/lib/video-recap/README.md +20 -16
  117. package/lib/video-recap/manifest.json +297 -321
  118. package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
  119. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
  120. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
  121. package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
  122. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
  123. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
  124. package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
  125. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
  126. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
  127. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
  128. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
  129. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
  130. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
  131. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
  132. package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
  133. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
  134. package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
  135. package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
  136. package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
  137. package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
  138. package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
  139. package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
  140. package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
  141. package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
  142. package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
  143. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
  144. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
  145. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
  146. package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
  147. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
  148. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
  149. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
  150. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
  151. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
  152. package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
  153. package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
  154. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
  155. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
  156. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
  157. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
  158. package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
  159. package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
  160. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
  161. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
  162. package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
  163. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
  164. package/lib/video-recap/skills/video-recap/SKILL.md +28 -29
  165. package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
  166. package/lib/video-recap/skills/video-recap/references/config-playbook.md +10 -13
  167. package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
  168. package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
  169. package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
  170. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
  171. package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
  172. package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
  173. package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
  174. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +55 -17
  175. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
  176. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +96 -117
  177. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
  178. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
  179. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
  180. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
  181. package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
  182. package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
  183. package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
  184. package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
  185. package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
  186. package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
  187. package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
  188. package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
  189. package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
  190. package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
  191. package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
  192. package/lib/video-recap/skills/video-script/SKILL.md +53 -24
  193. package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
  194. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
  195. package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
  196. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
  197. package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
  198. package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
  199. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
  200. package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
  201. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
  202. package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
  203. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -11
  204. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
  205. package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
  206. package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
  207. package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
  208. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
  209. package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
  210. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
  211. package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
  212. package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
  213. package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
  214. package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
  215. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
  216. package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
  217. package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
  218. package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
  219. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
  220. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
  221. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
  222. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
  223. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
  224. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
  225. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
  226. package/lib/video-recap/skills/video-voiceover/SKILL.md +51 -18
  227. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
  228. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
  229. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +81 -135
  230. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
  231. package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
  232. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
  233. package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
  234. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
  235. package/package.json +45 -45
  236. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  237. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  238. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  239. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
  240. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
  241. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
  242. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
  243. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
  244. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
  245. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
  246. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
  247. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
  248. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
  249. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
  250. package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
  251. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
  252. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
  253. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
  254. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
  255. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
  256. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
  257. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
  258. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
  259. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
  260. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
  261. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
  262. package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
  263. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
  264. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
  265. package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
  266. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
  267. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
  268. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
  269. /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
@@ -5,21 +5,26 @@ timing and text checks live here, and every later consumer trusts its result.
5
5
  """
6
6
 
7
7
  import json
8
- import math
9
8
  from pathlib import Path
10
9
 
11
10
  from lib import CONFIG, log
12
11
  from agent_text import (
13
- _clean_narration_punctuation,
12
+ TTS_UTTERANCE_OVERHEAD_SECONDS,
14
13
  _find_scene_for_midpoint,
15
- _normalise_narration_segment,
16
- _post_dedup_narration,
17
- _recommended_char_budget,
14
+ _lint_char_budget,
18
15
  _scene_available_seconds,
19
16
  _text_char_count,
20
- _truncate_at_sentence,
21
17
  )
22
18
  from deslop_qc import analyze_deslop_qc
19
+ from entry_suggestion import (
20
+ SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS,
21
+ _has_connected_predecessor,
22
+ _is_number,
23
+ _nearest_safe_anchor,
24
+ _other_block_spans,
25
+ _suggestion_window,
26
+ )
27
+ from lint_summary import format_lint_failure, format_lint_warnings
23
28
  from speech_ownership import (
24
29
  entry_overlaps_source_speech,
25
30
  load_source_sentence_evidence,
@@ -32,14 +37,6 @@ def _lint_issue(level, index, code, message, **extra):
32
37
  return issue
33
38
 
34
39
 
35
- def _is_number(value):
36
- return (
37
- isinstance(value, (int, float))
38
- and not isinstance(value, bool)
39
- and math.isfinite(value)
40
- )
41
-
42
-
43
40
  def _visual_overlay_issues(index, seg):
44
41
  """Shape-check the renderer handoff recap reads verbatim from a validated segment.
45
42
 
@@ -134,11 +131,16 @@ def _clip_matches_for_segment(seg, clip_plan, requested_clip_id):
134
131
  ]
135
132
 
136
133
 
137
- def _source_sentence_entry_issue(index, start, anchors, speech_owned):
134
+ def _source_sentence_entry_issue(
135
+ index, start, end, anchors, speech_owned, other_spans=(), window=(None, None),
136
+ ):
138
137
  """Return a blocking issue when narration enters midway through source speech.
139
138
 
140
139
  The validator reports a correction instead of silently moving audio. Source
141
140
  sentence integrity is invariant; an editorial policy string cannot bypass it.
141
+ `other_spans` are the other blocks' (start, end) and `window` the open range between
142
+ the neighbours (see _nearest_safe_anchor): the suggested entry never moves this block
143
+ onto, flush against, or past one of them.
142
144
  """
143
145
  if not speech_owned:
144
146
  return None
@@ -165,45 +167,81 @@ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
165
167
  if pause_start - 0.05 <= start <= when + 0.08:
166
168
  return None
167
169
 
168
- suggested = next(
169
- (anchor for anchor in anchors if anchor["time"] > start + 0.08), None
170
- )
170
+ suggested = _nearest_safe_anchor(start, end, anchors, other_spans, window)
171
171
  return _lint_issue(
172
172
  "error",
173
173
  index,
174
174
  "interrupts_source_sentence",
175
- "Narration enters before the source sentence finishes. Move this block to the suggested "
176
- "sentence-end anchor, or shorten/move/remove it when there is no later verified anchor, "
177
- "then rerun lint before TTS. Source sentence interruption has no override.",
175
+ "Narration enters before the source sentence finishes. Move the whole block (same "
176
+ "duration) to the suggested sentence-end anchor, or shorten/move/remove it when no "
177
+ f"anchor within {SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS:g}s keeps it between its neighbours "
178
+ "and clear of the other blocks, then rerun lint before TTS. The window stays inside the "
179
+ "block's clip (cut) or the output duration (cut_output); full mode does not know the "
180
+ "video's end, so keep it inside the video. Source sentence interruption has no override.",
178
181
  entry_time=round(start, 3),
179
182
  suggested_start=round(suggested["time"], 3) if suggested else None,
183
+ suggested_end=round(suggested["time"] + end - start, 3) if suggested else None,
184
+ max_shift_seconds=SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS,
180
185
  source_text_tail=str(suggested.get("text_tail", "")).strip() if suggested else "",
181
186
  anchor_confidence=suggested["confidence"] if suggested else None,
187
+ anchor_boundary_use=(
188
+ suggested.get("boundary_use")
189
+ or ("unverified" if suggested["confidence"] in {"high", "medium"} else "none")
190
+ )
191
+ if suggested
192
+ else None,
182
193
  )
183
194
 
184
195
 
185
- def _has_connected_predecessor(narration, idx, start):
186
- """A back-to-back narration handoff (<=150ms gap) does not expose the source track,
187
- so the next TTS block is not a new source-speech entry."""
188
- for other_idx, other in enumerate(narration):
189
- if other_idx == idx or not isinstance(other, dict):
190
- continue
191
- other_start, other_end = other.get("start"), other.get("end")
192
- if not _is_number(other_start) or not _is_number(other_end):
193
- continue
194
- if other_start < start and -0.001 <= start - other_end <= 0.15:
195
- return True
196
- return False
196
+ def _suggestion_limits(seg, clip_plan, mode, output_duration):
197
+ """Closed range a suggested move must stay in: the block's one clip (cut mode, source
198
+ time), or [0, output_duration] when that is known (cut_output)."""
199
+ if mode == "cut" and clip_plan:
200
+ requested = seg.get("source_clip_id")
201
+ try:
202
+ requested = None if requested is None else int(requested)
203
+ except (TypeError, ValueError):
204
+ return None, None
205
+ matches = _clip_matches_for_segment(seg, clip_plan, requested)
206
+ return _clip_span(matches[0]) if len(matches) == 1 else (None, None)
207
+ usable = _is_number(output_duration) and output_duration > 0
208
+ return 0.0, output_duration if usable else None
209
+
210
+
211
+ # Full-mode hard text budget: a segment longer than this multiple of its recommended
212
+ # character budget cannot be spoken in its window, so lint returns it to the author
213
+ # instead of anything downstream shortening it.
214
+ OVER_BUDGET_ERROR_RATIO = 1.25
215
+
216
+
217
+ def _over_budget_error(index, start, end, char_count, budget):
218
+ limit = int(budget * OVER_BUDGET_ERROR_RATIO)
219
+ return _lint_issue(
220
+ "error",
221
+ index,
222
+ "over_budget",
223
+ f"段 {index + 1} [{start:.2f}-{end:.2f}s] has {char_count} chars; the window holds "
224
+ f"about {budget} (hard limit {limit}), {char_count - limit} over the limit. "
225
+ "Shorten the text or widen/move the window, then rerun validate; "
226
+ "validation never shortens approved text.",
227
+ start=start,
228
+ end=end,
229
+ budget_chars=budget,
230
+ limit_chars=limit,
231
+ actual_chars=char_count,
232
+ over_chars=char_count - limit,
233
+ tts_overhead_seconds=TTS_UTTERANCE_OVERHEAD_SECONDS,
234
+ )
197
235
 
198
236
 
199
237
  def lint_narration(
200
238
  narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
201
- require_chronological=False,
239
+ output_duration=None,
202
240
  ):
203
241
  """Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set.
204
242
 
205
- Segments are sorted by start for the timing checks; ``require_chronological`` (the
206
- --preserve-approved-text contract) additionally rejects input that is not already in order."""
243
+ Segments must already be in chronological order; validation never reorders them.
244
+ `output_duration` (cut_output) bounds where interrupt suggestions may move a block."""
207
245
  scenes_analysis = scenes_analysis or []
208
246
  errors = []
209
247
  warnings = []
@@ -221,6 +259,8 @@ def lint_narration(
221
259
  )
222
260
  else:
223
261
  previous_start = None
262
+ # Suggested (start, end) of blocks already told to move; later blocks keep clear.
263
+ suggested_spans = {}
224
264
  for idx, seg in enumerate(narration):
225
265
  if not isinstance(seg, dict):
226
266
  errors.append(
@@ -240,7 +280,7 @@ def lint_narration(
240
280
  )
241
281
  )
242
282
  continue
243
- if require_chronological and previous_start is not None and start < previous_start:
283
+ if previous_start is not None and start < previous_start:
244
284
  errors.append(
245
285
  _lint_issue(
246
286
  "error",
@@ -304,12 +344,18 @@ def lint_narration(
304
344
  continue
305
345
 
306
346
  char_count = _text_char_count(text)
307
- budget = _recommended_char_budget(start, end)
347
+ budget = _lint_char_budget(start, end)
308
348
  # estimate at the REAL playback rate (after the narration_speed atempo); otherwise a
309
- # beat sized to its 1.3x-sped slot looks "over budget" when it actually fits.
349
+ # beat sized to its 1.3x-sped slot looks "over budget" when it actually fits. The
350
+ # utterance's TTS edge silence takes room too.
310
351
  play_rate = CONFIG["speech_rate"] * CONFIG["narration_speed"]
311
- estimated_tts_seconds = char_count / play_rate
352
+ estimated_tts_seconds = char_count / play_rate + TTS_UTTERANCE_OVERHEAD_SECONDS
312
353
  slot_seconds = _scene_available_seconds(start, end)
354
+ over_limit = (
355
+ mode == "full" and char_count > budget * OVER_BUDGET_ERROR_RATIO
356
+ )
357
+ if over_limit:
358
+ errors.append(_over_budget_error(idx, start, end, char_count, budget))
313
359
  if budget < 5:
314
360
  warnings.append(
315
361
  _lint_issue(
@@ -322,7 +368,9 @@ def lint_narration(
322
368
  budget_chars=budget,
323
369
  )
324
370
  )
325
- elif estimated_tts_seconds > slot_seconds:
371
+ # Independent of slot_too_short: a short slot can also be overfilled, and the
372
+ # author needs both numbers.
373
+ if not over_limit and estimated_tts_seconds > slot_seconds:
326
374
  warnings.append(
327
375
  _lint_issue(
328
376
  "warning",
@@ -352,15 +400,25 @@ def lint_narration(
352
400
  entry_issue = _source_sentence_entry_issue(
353
401
  idx,
354
402
  start,
403
+ end,
355
404
  source_sentence_anchors,
356
405
  entry_overlaps_source_speech(
357
406
  start,
358
407
  source_evidence,
359
408
  authored_overlap=seg.get("overlaps_speech", True),
360
409
  ),
410
+ _other_block_spans(narration, idx),
411
+ _suggestion_window(
412
+ narration, idx, suggested_spans,
413
+ _suggestion_limits(seg, clip_plan, mode, output_duration),
414
+ ),
361
415
  )
362
416
  if entry_issue:
363
417
  errors.append(entry_issue)
418
+ if entry_issue.get("suggested_end") is not None:
419
+ suggested_spans[idx] = (
420
+ entry_issue["suggested_start"], entry_issue["suggested_end"],
421
+ )
364
422
 
365
423
  scene_bounds = _scene_bounds_for_midpoint(scenes_analysis, start, end)
366
424
  if scenes_analysis and not scene_bounds:
@@ -404,6 +462,7 @@ def lint_narration(
404
462
  end=end,
405
463
  duration=round(end - start, 2),
406
464
  frame_fact_times=[round(ts, 2) for ts in frame_fact_times[:8]],
465
+ frame_fact_count=len(frame_fact_times),
407
466
  )
408
467
  )
409
468
 
@@ -603,11 +662,9 @@ def lint_narration(
603
662
  )
604
663
  )
605
664
 
665
+ # Unfiltered, so narration-sourced blocker indices are narration.json positions.
606
666
  deslop_qc = analyze_deslop_qc(
607
- [seg for seg in narration if isinstance(seg, dict)]
608
- if isinstance(narration, list)
609
- else [],
610
- work_dir=work_dir,
667
+ narration if isinstance(narration, list) else [], work_dir=work_dir,
611
668
  )
612
669
  for blocker in deslop_qc["blockers"]:
613
670
  errors.append(
@@ -632,76 +689,39 @@ def lint_narration(
632
689
  "warnings": warnings,
633
690
  }
634
691
  if work_dir is not None:
635
- Path(work_dir, "deslop_qc.json").write_text(
636
- json.dumps(deslop_qc, ensure_ascii=False, indent=2), encoding="utf-8"
637
- )
638
692
  Path(work_dir, "narration_lint.json").write_text(
639
693
  json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
640
694
  )
641
695
  return report
642
696
 
643
697
 
698
+ class NarrationLintError(ValueError):
699
+ """Lint found blocking errors: an ordinary result for the author to fix, not a crash.
700
+
701
+ str() is the compact per-block summary; `report` is the full lint report."""
702
+
703
+ def __init__(self, report, lint_path=None):
704
+ self.report = report
705
+ super().__init__(format_lint_failure(report, lint_path))
706
+
707
+
644
708
  def validate_narration_or_raise(
645
709
  narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
646
- require_chronological=False,
710
+ output_duration=None,
647
711
  ):
648
712
  report = lint_narration(
649
713
  narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir,
650
- require_chronological=require_chronological,
714
+ output_duration=output_duration,
651
715
  )
652
716
  if report["errors"]:
653
- sample = "; ".join(
654
- f"#{e['index']}: {e['code']}" for e in report["errors"][:3]
717
+ raise NarrationLintError(
718
+ report, None if work_dir is None else Path(work_dir, "narration_lint.json")
655
719
  )
656
- raise ValueError(f"narration.json 预检失败: {sample}; 详见 narration_lint.json")
657
720
  if report["warnings"]:
658
- log(
659
- f"narration lint: {len(report['warnings'])} warnings (see narration_lint.json)"
660
- )
721
+ log(format_lint_warnings(
722
+ report, None if work_dir is None else Path(work_dir, "narration_lint.json")
723
+ ))
661
724
  else:
662
- log("narration lint: ok")
725
+ log("narration lint:通过")
663
726
  return report
664
727
 
665
-
666
- def _validate_narration_budget(narration, scenes_analysis):
667
- """Trim lint-validated narration to its timing budgets; drop what cannot be spoken."""
668
- del scenes_analysis # scene boundaries are advisory (lint warns); authored timing is kept
669
- cleaned = []
670
- for raw in narration:
671
- item = _normalise_narration_segment(raw)
672
- max_chars = _recommended_char_budget(item["start"], item["end"])
673
- if max_chars < 5:
674
- log(f" 丢弃过短解说段 {item['start']:.1f}-{item['end']:.1f}s")
675
- continue
676
- if _text_char_count(item["narration"]) > max_chars * 1.25:
677
- truncated = _truncate_at_sentence(item["narration"], max_chars)
678
- if truncated and _text_char_count(truncated) >= 5:
679
- log(f" 解说超预算,已截短: {item['start']:.1f}-{item['end']:.1f}s")
680
- item["narration"] = truncated
681
- else:
682
- log(
683
- f" 解说超预算且无法安全截断,已丢弃: {item['start']:.1f}-{item['end']:.1f}s"
684
- )
685
- continue
686
- item["narration"] = _clean_narration_punctuation(item["narration"])
687
- stripped = item["narration"].strip()
688
- if stripped and stripped[-1] in ",:、;,—":
689
- item["narration"] = stripped.rstrip(",:、;,—") + "。"
690
- cleaned.append(item)
691
-
692
- cleaned.sort(key=lambda n: n["start"])
693
- deduped = []
694
- for item in cleaned:
695
- if deduped and item["start"] < deduped[-1]["end"]:
696
- prev = deduped[-1]
697
- log(
698
- f" 解说时间重叠: {item['start']:.1f}-{item['end']:.1f}s vs "
699
- f"{prev['start']:.1f}-{prev['end']:.1f}s"
700
- )
701
- if _text_char_count(item["narration"]) > _text_char_count(
702
- prev["narration"]
703
- ):
704
- deduped[-1] = item
705
- else:
706
- deduped.append(item)
707
- return _post_dedup_narration(deduped)
@@ -1,40 +1,9 @@
1
1
  #!/usr/bin/env python3
2
- """Public API and CLI entrypoint for narration review."""
2
+ """CLI entrypoint for narration review."""
3
3
 
4
- from evidence_bundle import (
5
- build_evidence_bundle,
6
- build_review_coverage_metadata,
7
- coverage_policy_v1,
8
- filter_evidence_by_ranges,
9
- render_evidence_bundle,
10
- )
11
- from review_grounding import remap_grounding_to_output_timeline
12
- from review_response import (
13
- build_grounding_qc,
14
- build_review_messages,
15
- format_review_md,
16
- merge_review_findings,
17
- parse_review_response,
18
- write_grounding_qc,
19
- )
20
- from review_runner import main, review_narration
4
+ from review_runner import main
21
5
 
22
- __all__ = [
23
- "build_evidence_bundle",
24
- "build_grounding_qc",
25
- "build_review_coverage_metadata",
26
- "build_review_messages",
27
- "coverage_policy_v1",
28
- "filter_evidence_by_ranges",
29
- "format_review_md",
30
- "main",
31
- "merge_review_findings",
32
- "parse_review_response",
33
- "remap_grounding_to_output_timeline",
34
- "render_evidence_bundle",
35
- "review_narration",
36
- "write_grounding_qc",
37
- ]
6
+ __all__ = ["main"]
38
7
 
39
8
  if __name__ == "__main__":
40
9
  main()
@@ -50,44 +50,30 @@ def _load_review_grounding(work_dir):
50
50
  """
51
51
  work_dir = Path(work_dir)
52
52
  manifest = _load(work_dir, "multi_source_manifest.json")
53
- sources = manifest.get("sources") if isinstance(manifest, dict) else None
54
- if isinstance(sources, list):
53
+ if manifest is not None:
55
54
  combined_vlm = []
56
55
  combined_asr = []
57
- for source in sources:
58
- if not isinstance(source, dict):
59
- continue
60
- source_id = str(source.get("source_id") or "").strip()
61
- relative_dir = source.get("source_work_dir")
62
- if not source_id or not relative_dir:
63
- continue
64
- source_dir = (work_dir / str(relative_dir)).resolve(strict=False)
56
+ for source in manifest["sources"]:
57
+ source_id = str(source["source_id"])
58
+ source_dir = (work_dir / source["source_work_dir"]).resolve(strict=False)
65
59
  try:
66
60
  source_dir.relative_to(work_dir.resolve(strict=False))
67
61
  except ValueError:
68
62
  continue
69
63
  for scene in _load(source_dir, "vlm_analysis.json") or []:
70
- if isinstance(scene, dict):
71
- item = dict(scene)
72
- item["source_id"] = source_id
73
- combined_vlm.append(item)
74
- clean_asr = _load(source_dir, "asr_clean.json")
75
- raw_asr = (
76
- clean_asr
77
- if clean_asr is not None
78
- else _load(source_dir, "asr_result.json")
79
- )
80
- for segment in _asr_segments(raw_asr):
81
- if isinstance(segment, dict):
82
- item = dict(segment)
83
- item["source_id"] = source_id
84
- combined_asr.append(item)
64
+ combined_vlm.append({**scene, "source_id": source_id})
65
+ for segment in _preferred_asr(source_dir):
66
+ combined_asr.append({**segment, "source_id": source_id})
85
67
  if combined_vlm or combined_asr:
86
68
  return combined_vlm, combined_asr
87
69
 
88
- return _load(work_dir, "vlm_analysis.json") or [], _asr_segments(
89
- _load(work_dir, "asr_result.json")
90
- )
70
+ return _load(work_dir, "vlm_analysis.json") or [], _preferred_asr(work_dir)
71
+
72
+
73
+ def _preferred_asr(work_dir):
74
+ """asr_clean.json wins over asr_result.json, as in lint, cut and assemble."""
75
+ clean = _load(work_dir, "asr_clean.json")
76
+ return _asr_segments(clean if clean is not None else _load(work_dir, "asr_result.json"))
91
77
 
92
78
 
93
79
  def _load_cut_clip_spans(work_dir):
@@ -176,13 +162,8 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
176
162
 
177
163
  remapped_scenes = []
178
164
  for scene in vlm_analysis or []:
179
- if not isinstance(scene, dict):
180
- continue
181
- try:
182
- start = float(scene.get("start", 0))
183
- end = float(scene.get("end", start))
184
- except (TypeError, ValueError):
185
- continue
165
+ start = float(scene["start"])
166
+ end = float(scene["end"])
186
167
  overlaps = _source_output_overlaps(
187
168
  start,
188
169
  end,
@@ -205,13 +186,8 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
205
186
 
206
187
  remapped_asr = []
207
188
  for seg in asr_result or []:
208
- if not isinstance(seg, dict):
209
- continue
210
- try:
211
- start = float(seg.get("start", 0))
212
- end = float(seg.get("end", start))
213
- except (TypeError, ValueError):
214
- continue
189
+ start = float(seg["start"])
190
+ end = float(seg["end"])
215
191
  text = str(seg.get("text", "")).strip()
216
192
  if not text:
217
193
  continue
@@ -231,8 +207,6 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
231
207
  item["output_segment_index"] = overlap.get("output_segment_index")
232
208
  remapped_asr.append(item)
233
209
 
234
- remapped_scenes.sort(
235
- key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0)))
236
- )
237
- remapped_asr.sort(key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0))))
210
+ remapped_scenes.sort(key=lambda x: (x["start"], x["end"]))
211
+ remapped_asr.sort(key=lambda x: (x["start"], x["end"]))
238
212
  return remapped_scenes, remapped_asr