@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
@@ -0,0 +1,125 @@
1
+ """Where an interrupting narration block may be told to move.
2
+
3
+ `interrupts_source_sentence` suggests a sentence-end anchor the whole block can shift to.
4
+ These helpers keep that suggestion between the block's neighbours and clear of every other
5
+ block, and decide when a block is a back-to-back handoff that needs no entry check.
6
+ """
7
+
8
+ import math
9
+
10
+
11
+ def _is_number(value):
12
+ return (
13
+ isinstance(value, (int, float))
14
+ and not isinstance(value, bool)
15
+ and math.isfinite(value)
16
+ )
17
+
18
+
19
+ # A narration block that starts within this gap after another block's end is a
20
+ # back-to-back handoff: the source track is never exposed between the two.
21
+ CONNECTED_HANDOFF_SECONDS = 0.15
22
+ # A suggested entry farther than this from the authored one moves the block off the
23
+ # pictures it was written for; past it the author must rethink the block instead.
24
+ SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS = 10.0
25
+
26
+
27
+ def _shift_is_clear(new_start, duration, other_spans):
28
+ """The block moved whole to new_start neither overlaps nor abuts another block."""
29
+ new_end = new_start + duration
30
+ gap = CONNECTED_HANDOFF_SECONDS
31
+ return all(
32
+ new_end < other_start - gap or new_start > other_end + gap
33
+ for other_start, other_end in other_spans
34
+ )
35
+
36
+
37
+ def _nearest_safe_anchor(start, end, anchors, other_spans, window=(None, None)):
38
+ """The anchor closest to `start` the whole block can move to (duration kept) without
39
+ overlapping or abutting another block or leaving `window`, within
40
+ SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS; a later anchor wins a tie. None when none qualifies.
41
+
42
+ `window` is the open (low, high) range the moved block must sit strictly inside: the
43
+ neighbours' ends with the handoff gap, so a block never jumps over a neighbour (story
44
+ order) and two adjacent blocks never get colliding suggestions. Either side may be None.
45
+ """
46
+ duration = end - start
47
+ low, high = window
48
+ candidates = [
49
+ anchor
50
+ for anchor in anchors
51
+ if abs(anchor["time"] - start) <= SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS
52
+ and anchor["time"] >= 0
53
+ and (low is None or anchor["time"] > low)
54
+ and (high is None or anchor["time"] + duration < high)
55
+ and _shift_is_clear(anchor["time"], duration, other_spans)
56
+ ]
57
+ if not candidates:
58
+ return None
59
+ return min(candidates, key=lambda anchor: (abs(anchor["time"] - start), -anchor["time"]))
60
+
61
+
62
+ def _valid_span(seg):
63
+ """(start, end) of a block with a usable numeric window, else None."""
64
+ if not isinstance(seg, dict):
65
+ return None
66
+ start, end = seg.get("start"), seg.get("end")
67
+ if _is_number(start) and _is_number(end) and end > start:
68
+ return start, end
69
+ return None
70
+
71
+
72
+ def _other_block_spans(narration, idx):
73
+ """(start, end) of every other block with a usable numeric window."""
74
+ return [
75
+ span
76
+ for other_idx, other in enumerate(narration)
77
+ if other_idx != idx and (span := _valid_span(other))
78
+ ]
79
+
80
+
81
+ # Clip/timeline edges are allowed positions; the open window bound sits this far outside.
82
+ _EDGE_EPSILON = 1e-6
83
+
84
+
85
+ def _suggestion_window(narration, idx, suggested_spans, limits=(None, None)):
86
+ """Open (low, high) range a suggested move of block `idx` must stay strictly inside.
87
+
88
+ Bounded by the previous and next valid blocks (narration is chronological) with the
89
+ handoff gap. The previous block counts at both its authored and its own suggested
90
+ window, so applying any subset of the suggestions keeps adjacent blocks apart and in
91
+ order. `limits` is the closed (first, last) range the block must stay in: its clip in
92
+ cut mode, the output duration in cut_output; either side may be None.
93
+ """
94
+ gap = CONNECTED_HANDOFF_SECONDS
95
+ low = high = None
96
+ for prev_idx in range(idx - 1, -1, -1):
97
+ span = _valid_span(narration[prev_idx])
98
+ if span:
99
+ low = max(span[1], suggested_spans.get(prev_idx, span)[1]) + gap
100
+ break
101
+ for next_idx in range(idx + 1, len(narration)):
102
+ span = _valid_span(narration[next_idx])
103
+ if span:
104
+ high = span[0] - gap
105
+ break
106
+ first, last = limits
107
+ if first is not None:
108
+ low = max(x for x in (low, first - _EDGE_EPSILON) if x is not None)
109
+ if last is not None:
110
+ high = min(x for x in (high, last + _EDGE_EPSILON) if x is not None)
111
+ return low, high
112
+
113
+
114
+ def _has_connected_predecessor(narration, idx, start):
115
+ """A back-to-back narration handoff (<=150ms gap) does not expose the source track,
116
+ so the next TTS block is not a new source-speech entry."""
117
+ for other_idx, other in enumerate(narration):
118
+ if other_idx == idx or not isinstance(other, dict):
119
+ continue
120
+ other_start, other_end = other.get("start"), other.get("end")
121
+ if not _is_number(other_start) or not _is_number(other_end):
122
+ continue
123
+ if other_start < start and -0.001 <= start - other_end <= CONNECTED_HANDOFF_SECONDS:
124
+ return True
125
+ return False
@@ -7,13 +7,11 @@ import re
7
7
 
8
8
  EVIDENCE_CONTRACT_VERSION = 1
9
9
 
10
- COVERAGE_POLICY_VERSION = "coverage_policy_v1"
11
-
12
10
 
13
11
  def _safe_time(item, key, default=0.0):
14
12
  try:
15
13
  value = float(item.get(key, default))
16
- except (AttributeError, TypeError, ValueError):
14
+ except (TypeError, ValueError):
17
15
  return default
18
16
  return value if math.isfinite(value) else default
19
17
 
@@ -85,7 +83,6 @@ def coverage_policy_v1(
85
83
  duration = _overall_duration(scenes, asr_result, narration)
86
84
  if duration <= 0:
87
85
  return {
88
- "coverage_policy_version": COVERAGE_POLICY_VERSION,
89
86
  "selected_ranges": [],
90
87
  "dropped_ranges": [],
91
88
  "dropped_range_count": 0,
@@ -162,7 +159,6 @@ def coverage_policy_v1(
162
159
  for r in merged + dropped:
163
160
  r.pop("priority", None)
164
161
  return {
165
- "coverage_policy_version": COVERAGE_POLICY_VERSION,
166
162
  "selected_ranges": merged,
167
163
  "dropped_ranges": dropped,
168
164
  "dropped_range_count": len(dropped),
@@ -276,13 +272,11 @@ def _research_context_items(research):
276
272
 
277
273
 
278
274
  def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="source"):
279
- """Pure compatibility seam: collect visual/ASR evidence items inside ranges."""
275
+ """Pure: collect visual/ASR evidence items inside ranges."""
280
276
  clock = "output" if timeline == "cut_output" else "source"
281
277
  items = []
282
278
  dropped = {"visual": 0, "asr": 0}
283
279
  for i, scene in enumerate(vlm_analysis or []):
284
- if not isinstance(scene, dict):
285
- continue
286
280
  start = _safe_time(scene, "start")
287
281
  end = _safe_time(scene, "end", start)
288
282
  if end <= start or not _in_ranges(start, end, ranges):
@@ -309,8 +303,6 @@ def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="sou
309
303
  item[k] = scene.get(k)
310
304
  items.append(item)
311
305
  for i, seg in enumerate(asr_result or []):
312
- if not isinstance(seg, dict):
313
- continue
314
306
  text = str(seg.get("text", "")).strip()
315
307
  if not text:
316
308
  continue
@@ -346,22 +338,6 @@ def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="sou
346
338
  }
347
339
 
348
340
 
349
- def build_review_coverage_metadata(bundle):
350
- """Summarize a build_evidence_bundle() bundle's coverage contract."""
351
- coverage = bundle["coverage"]
352
- metadata = bundle["metadata"]
353
- return {
354
- "coverage_policy_version": coverage["coverage_policy_version"],
355
- "time_ranges": coverage["selected_ranges"],
356
- "dropped_ranges": coverage["dropped_ranges"],
357
- "dropped_range_count": coverage["dropped_range_count"],
358
- "scene_count": metadata["reviewed_scene_count"],
359
- "asr_count": metadata["reviewed_asr_count"],
360
- "dropped_scene_count": metadata["dropped_scene_count"],
361
- "dropped_asr_count": metadata["dropped_asr_count"],
362
- }
363
-
364
-
365
341
  def build_evidence_bundle(
366
342
  vlm_analysis,
367
343
  asr_result,
@@ -73,11 +73,6 @@ def _env_number(name, default, cast, minimum):
73
73
  return value
74
74
 
75
75
 
76
- def env_int(name, default, *, minimum=None):
77
- """Read an integer env var, rejecting malformed or below-minimum values."""
78
- return _env_number(name, default, int, minimum)
79
-
80
-
81
76
  def env_bool(name, default=False):
82
77
  """Read common boolean env var forms."""
83
78
  raw = os.environ.get(name)
@@ -120,9 +115,6 @@ CONFIG = {
120
115
  "quiet_overlap_min_ratio": 0.8, # 解说段至少多少比例落在安静窗口内才标记为非对白重叠
121
116
  "visual_beat_max_seconds": 18.0, # 单段解说超过该时长且跨多个帧锚点时给 lint 提醒
122
117
  "visual_beat_max_facts": 3, # 单段解说最多建议覆盖的 frame_facts 锚点数量
123
- "asr_chunk_min_chars": env_int("ASR_CHUNK_MIN_CHARS", 500, minimum=1), # brief 中 ASR 写作分块最小字数/词数
124
- "asr_chunk_max_chars": env_int("ASR_CHUNK_MAX_CHARS", 800, minimum=1), # brief 中 ASR 写作分块最大字数/词数
125
- "edit_mode": os.environ.get("EDIT_MODE", "full"), # full | cut
126
118
  }
127
119
 
128
120
  def log(msg):
@@ -165,9 +157,8 @@ def _sanitize_api_error(value, limit=500):
165
157
  text = _ERROR_KEY_RE.sub("<redacted-key>", text)
166
158
  return text[:limit]
167
159
 
168
- def _api_headers(api_provider=None, api_url=None, api_key=None):
160
+ def _api_headers(api_key=None):
169
161
  """Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
170
- del api_provider, api_url # MiMo is the only provider; signature kept for call sites
171
162
  key = CONFIG["api_key"] if api_key is None else api_key
172
163
  return {
173
164
  "Content-Type": "application/json",
@@ -175,9 +166,8 @@ def _api_headers(api_provider=None, api_url=None, api_key=None):
175
166
  "api-key": key,
176
167
  }
177
168
 
178
- def _prepare_api_payload(payload, api_provider=None, api_url=None):
169
+ def _prepare_api_payload(payload):
179
170
  """Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
180
- del api_provider, api_url
181
171
  normalized = dict(payload)
182
172
  if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
183
173
  normalized["max_completion_tokens"] = normalized.pop("max_tokens")
@@ -192,15 +182,15 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
192
182
  normalized["thinking"] = {"type": "disabled"}
193
183
  return normalized
194
184
 
195
- def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_env_var=None):
185
+ def api_call(payload, max_retries=8, *, api_url=None, api_key=None, api_env_var=None):
196
186
  """调用 OpenAI-compatible API,带重试。
197
187
 
198
188
  集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
199
189
  避免一次瞬时限流就中止整个阶段。配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s。
200
190
  """
201
191
  endpoint = normalize_api_url(api_url if api_url is not None else CONFIG["api_url"])
202
- headers = _api_headers(api_provider=api_provider, api_url=endpoint, api_key=api_key)
203
- data = json.dumps(_prepare_api_payload(payload, api_provider=api_provider, api_url=endpoint)).encode("utf-8")
192
+ headers = _api_headers(api_key=api_key)
193
+ data = json.dumps(_prepare_api_payload(payload)).encode("utf-8")
204
194
 
205
195
  for attempt in range(max_retries):
206
196
  try:
@@ -0,0 +1,184 @@
1
+ """Render a narration lint report as the compact lines the author reads on the console.
2
+
3
+ narration_lint.json keeps every field; this summary repeats, per flagged block, the
4
+ numbers needed to fix it without opening that file. Errors (failed lint) and warnings
5
+ (passed lint) use the same line format. Block numbers are 1-based ("段 N",
6
+ the block's position in narration.json); the report's `index` fields stay 0-based. A
7
+ deslop blocker found in original_subtitles.json carries `source: "original_subtitles"`
8
+ and an index into that file, so it is labelled "原声字幕第 N 条", never "段 N".
9
+ """
10
+
11
+ MAX_LISTED_ERRORS = 12
12
+
13
+ _HINTS = {
14
+ "over_budget": "缩短文字,或放宽/挪动时间窗",
15
+ "interrupts_source_sentence": "把整块(时长不变)挪到建议的时间窗;没有建议入点时缩短、挪走或删掉这块",
16
+ "source_sentence_anchors_unavailable": "挪走或删掉这块,或重新生成含句尾锚点的语音证据",
17
+ "time_overlap": "调整时间窗,让两块不重叠",
18
+ "out_of_order": "按 start 从小到大排列各块",
19
+ "outside_clip_plan": "把这块挪进某个选中片段的时间范围",
20
+ "ambiguous_source_clip": "给这块写 source_clip_id",
21
+ "em_dash": "删掉破折号,改用逗号、句号或把句子拆开",
22
+ "placeholder_leakage": "把示例占位(如【主角】)换成真实人名或内容",
23
+ "slot_too_short": "放宽时间窗,或把这块并进相邻块",
24
+ "incomplete_sentence": "用句号、问号或感叹号收尾",
25
+ "outside_scene": "核对时间,确认这块对应哪段画面",
26
+ "crosses_scene_boundary": "把这块收进所在场景,或在场景切换处拆成两块",
27
+ "visual_beat_too_broad": "拆块或收紧时间窗,让旁白贴着当前画面",
28
+ "crosses_clip_boundary": "把这块收进所在片段,超出部分会被裁掉",
29
+ "no_original_blocks": "在几个强原声时刻不写旁白,让原声满音量播放",
30
+ "under_narrated": "确认每个长空档都有意交给原声;只在有明确解说任务时加块",
31
+ "no_original_breaks": "在块之间留几个多秒的空档给原声",
32
+ "fragmented_beats": "把相邻、讲同一件事的短句并成一块",
33
+ }
34
+
35
+ _ORIGINAL_SUBTITLE_HINTS = {
36
+ "em_dash": "在 original_subtitles.json 里删掉这条的破折号(不是改 narration.json)",
37
+ "placeholder_leakage": "在 original_subtitles.json 里替换这条的示例占位(不是改 narration.json)",
38
+ }
39
+ _ORIGINAL_SUBTITLE_FALLBACK_HINT = "改 original_subtitles.json 里的这一条,不是 narration.json"
40
+
41
+
42
+ def block_label(index):
43
+ """'段 N' for a 0-based block index; '整体' for a file-level issue."""
44
+ return "整体" if index is None else f"段 {index + 1}"
45
+
46
+
47
+ def _label(e):
48
+ if e.get("source") == "original_subtitles":
49
+ index = e.get("index")
50
+ return "原声字幕" if index is None else f"原声字幕第 {index + 1} 条"
51
+ return block_label(e.get("index"))
52
+
53
+
54
+ def _hint(e):
55
+ code = e.get("code")
56
+ if e.get("source") == "original_subtitles":
57
+ return _ORIGINAL_SUBTITLE_HINTS.get(code, _ORIGINAL_SUBTITLE_FALLBACK_HINT)
58
+ return _HINTS.get(code)
59
+
60
+
61
+ def _seconds(value):
62
+ return f"{value:.2f}s" if isinstance(value, (int, float)) else "?"
63
+
64
+
65
+ def _window(e):
66
+ return f"{_seconds(e.get('start'))}-{_seconds(e.get('end'))}"
67
+
68
+
69
+ def _over_budget(e):
70
+ head = f"{_window(e)} 写了 {e.get('actual_chars')} 字,窗口约 {e.get('budget_chars')} 字"
71
+ if "limit_chars" in e: # the full-mode error
72
+ return f"{head}(硬上限 {e.get('limit_chars')},超出 {e.get('over_chars')} 字)"
73
+ return (
74
+ f"{head},估计读完 {_seconds(e.get('estimated_tts_seconds'))},"
75
+ f"可用 {_seconds(e.get('slot_seconds'))}"
76
+ )
77
+
78
+
79
+ def _interrupts(e):
80
+ text = f"入点 {_seconds(e.get('entry_time'))} 落在原声句子中间"
81
+ if e.get("suggested_start") is None:
82
+ shift = e.get("max_shift_seconds")
83
+ within = f"前后 {shift:g} 秒内" if isinstance(shift, (int, float)) else "附近"
84
+ return text + f",{within}没有能整块挪过去、留在前后两块之间又不与其他块重叠或相接的句尾锚点"
85
+ tail = e.get("source_text_tail")
86
+ tail = f",句尾「{tail}」" if tail else ""
87
+ target = _seconds(e["suggested_start"])
88
+ if e.get("suggested_end") is not None:
89
+ target += f"-{_seconds(e['suggested_end'])}"
90
+ return (
91
+ f"{text},建议入点 {target}"
92
+ f"(锚点 {e.get('anchor_confidence')}/{e.get('anchor_boundary_use')}{tail})"
93
+ )
94
+
95
+
96
+ def _ratio(value):
97
+ return f"{value:.0%}" if isinstance(value, (int, float)) else "?"
98
+
99
+
100
+ _WARNING_DETAILS = {
101
+ "slot_too_short": lambda e: f"{_window(e)} 只容得下约 {e.get('budget_chars')} 字,TTS 可能放不下",
102
+ "incomplete_sentence": lambda e: f"结尾「{e.get('text_tail', '')}」没有句末标点",
103
+ "outside_scene": lambda e: f"{_window(e)} 的中点不在任何检测到的场景里",
104
+ "crosses_scene_boundary": lambda e: (
105
+ f"{_window(e)} 超出所在场景 "
106
+ f"{_seconds(e.get('scene_start'))}-{_seconds(e.get('scene_end'))}"
107
+ ),
108
+ "visual_beat_too_broad": lambda e: (
109
+ f"{_window(e)} 共 {_seconds(e.get('duration'))},"
110
+ f"跨过 {e.get('frame_fact_count', '?')} 个画面锚点"
111
+ ),
112
+ "crosses_clip_boundary": lambda e: (
113
+ f"{_window(e)} 超出所在片段 {_seconds(e.get('clip_start'))}-{_seconds(e.get('clip_end'))}"
114
+ ),
115
+ "no_original_blocks": lambda e: (
116
+ f"旁白覆盖率 {_ratio(e.get('narration_coverage'))},超过上限 {_ratio(e.get('coverage_max'))}"
117
+ ),
118
+ "under_narrated": lambda e: (
119
+ f"旁白覆盖率 {_ratio(e.get('narration_coverage'))},低于下限 {_ratio(e.get('coverage_min'))}"
120
+ ),
121
+ "no_original_breaks": lambda e: (
122
+ f"块与块之间没有 ≥{_seconds(e.get('original_block_min_seconds'))} 的原声段"
123
+ ),
124
+ "fragmented_beats": lambda e: (
125
+ f"平均每块 {e.get('avg_block_chars')} 字,少于 {e.get('block_min_chars')} 字"
126
+ ),
127
+ }
128
+
129
+
130
+ def _details(e):
131
+ code = e.get("code")
132
+ if code == "over_budget" and "budget_chars" in e:
133
+ return _over_budget(e)
134
+ if code == "interrupts_source_sentence":
135
+ return _interrupts(e)
136
+ if code == "source_sentence_anchors_unavailable":
137
+ return f"入点 {_seconds(e.get('entry_time'))} 压在原声上,但没有已验证的句尾锚点"
138
+ if code == "time_overlap":
139
+ return (
140
+ f"{_seconds(e.get('start'))}-{_seconds(e.get('end'))} 与"
141
+ f"{block_label(e.get('previous_index'))}(结束于 {_seconds(e.get('previous_end'))})重叠"
142
+ )
143
+ if code == "out_of_order":
144
+ return f"start {_seconds(e.get('start'))} 早于上一块的 {_seconds(e.get('previous_start'))}"
145
+ if code in _WARNING_DETAILS:
146
+ return _WARNING_DETAILS[code](e)
147
+ return str(e.get("message", "")).strip()
148
+
149
+
150
+ def _issue_line(e):
151
+ line = f"- {_label(e)} {e.get('code')}:{_details(e)}"
152
+ hint = _hint(e)
153
+ if hint:
154
+ line += f"。改法:{hint}"
155
+ return line
156
+
157
+
158
+ def _warning_lines(report):
159
+ """One line per warning, every warning listed: they never block, so none is cut."""
160
+ return [_issue_line(w) for w in report.get("warnings", [])]
161
+
162
+
163
+ def format_lint_warnings(report, lint_path=None):
164
+ """The console summary of a lint that passed with warnings."""
165
+ lines = [f"narration lint:通过,{len(report.get('warnings', []))} 个 warning(不阻塞)"]
166
+ lines.extend(_warning_lines(report))
167
+ if lint_path is not None:
168
+ lines.append(f"完整报告:{lint_path}")
169
+ return "\n".join(lines)
170
+
171
+
172
+ def format_lint_failure(report, lint_path=None):
173
+ errors = report.get("errors", [])
174
+ lines = [f"narration.json 预检失败:{len(errors)} 个 error,修改后重跑 validate。"]
175
+ lines.extend(_issue_line(e) for e in errors[:MAX_LISTED_ERRORS])
176
+ if len(errors) > MAX_LISTED_ERRORS:
177
+ lines.append(f"- …另有 {len(errors) - MAX_LISTED_ERRORS} 个 error 未列出")
178
+ warnings = _warning_lines(report)
179
+ if warnings:
180
+ lines.append(f"另有 {len(warnings)} 个 warning(不阻塞):")
181
+ lines.extend(warnings)
182
+ if lint_path is not None:
183
+ lines.append(f"完整报告(含全部字段):{lint_path}")
184
+ return "\n".join(lines)