@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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import math
5
+ import os
5
6
  import shlex
6
7
  import shutil
7
8
  import subprocess
@@ -9,8 +10,7 @@ import sys
9
10
  from pathlib import Path
10
11
 
11
12
  import materials as material_lib
12
- from doctor import ffmpeg_has_subtitles_filter
13
- from lib import env_bool, env_int, load_json
13
+ from lib import env_bool, env_int, ffmpeg_has_subtitles_filter, file_identity, load_json, material_id_for
14
14
  from recap_source import audio_binding
15
15
 
16
16
  BUNDLE = Path(__file__).resolve().parents[2] # the skills/ directory
@@ -23,7 +23,9 @@ MULTI_SOURCE_MANIFEST = "multi_source_manifest.json"
23
23
  def _run(skill, script, *cli_args):
24
24
  cmd = [sys.executable, str(_entry(skill, script)), *map(str, cli_args)]
25
25
  print(f"[video-recap] ▶ {skill}/{script}", flush=True)
26
- res = subprocess.run(cmd)
26
+ # Stage scripts log Chinese; on Windows a piped stdout defaults to cp1252 and the
27
+ # first log line would crash the child with UnicodeEncodeError.
28
+ res = subprocess.run(cmd, env={**os.environ, "PYTHONIOENCODING": "utf-8"})
27
29
  if res.returncode != 0:
28
30
  raise SystemExit(f"{skill}/{script} 失败 (exit {res.returncode})")
29
31
 
@@ -60,12 +62,18 @@ def _read_video_duration_or_raise(path):
60
62
  return duration
61
63
 
62
64
 
63
- def _probe_display_height_or_raise(path, *, require_square_pixels=False):
65
+ # Subtitle Y coordinates are display-canvas rows; a near-square SAR only changes the canvas
66
+ # width (or, rotated, the row scale the renderer converts exactly). Same value as the
67
+ # renderer's and the measuring tool's copies (parity-tested).
68
+ SUBTITLE_BAND_SAR_TOLERANCE = 0.02
69
+
70
+
71
+ def _probe_display_height_or_raise(path, *, require_near_square_pixels=False):
64
72
  """Return ffmpeg's display-coordinate height, accounting for rotation and SAR."""
65
- return _probe_display_size_or_raise(path, require_square_pixels=require_square_pixels)[1]
73
+ return _probe_display_size_or_raise(path, require_near_square_pixels=require_near_square_pixels)[1]
66
74
 
67
75
 
68
- def _probe_display_size_or_raise(path, *, require_square_pixels=False):
76
+ def _probe_display_size_or_raise(path, *, require_near_square_pixels=False):
69
77
  """Return ffmpeg's display-coordinate (width, height), accounting for rotation and SAR."""
70
78
  cmd = [
71
79
  "ffprobe",
@@ -92,11 +100,14 @@ def _probe_display_size_or_raise(path, *, require_square_pixels=False):
92
100
  sar_ratio = float(num) / float(den)
93
101
  except (ValueError, ZeroDivisionError):
94
102
  sar_ratio = math.nan
95
- if require_square_pixels and (
96
- not math.isfinite(sar_ratio) or abs(sar_ratio - 1.0) >= 1e-9
103
+ if sar in {"0:1", "N/A"}:
104
+ sar_ratio = 1.0 # unspecified SAR: ffmpeg (and the renderer's canvas) treat it as square
105
+ if require_near_square_pixels and (
106
+ not math.isfinite(sar_ratio) or abs(sar_ratio - 1.0) > SUBTITLE_BAND_SAR_TOLERANCE + 1e-9
97
107
  ):
98
108
  raise SystemExit(
99
- f"subtitle Y coordinates currently require square-pixel video (SAR 1:1); got {sar}"
109
+ "subtitle Y coordinates require square or near-square pixels "
110
+ f"(SAR within {SUBTITLE_BAND_SAR_TOLERANCE:.0%} of 1:1); got {sar}"
100
111
  )
101
112
  display_width = max(1, round(width * sar_ratio)) if math.isfinite(sar_ratio) else width
102
113
  rotation_values = [
@@ -134,14 +145,43 @@ def _run_manifest_payload(video, args):
134
145
  return {
135
146
  "schema_version": 1,
136
147
  "source_video": str(Path(video).resolve()),
137
- "source_video_identity": material_lib.file_identity(video),
148
+ "source_video_identity": file_identity(video),
138
149
  "settings": _analysis_settings(args),
139
150
  "audio": audio_binding(args),
140
151
  }
141
152
 
142
153
 
154
+ # Settings main() can take from the environment rather than argv, with their no-env value.
155
+ # A resume shell may not carry the same environment, so the resume argv pins them.
156
+ _ENV_FILLED = (
157
+ ("edit_mode", "full"),
158
+ ("target_duration", None),
159
+ ("tts_provider", "auto"),
160
+ ("voice_ref", None),
161
+ ("subtitle_y_top", None),
162
+ ("subtitle_y_bot", None),
163
+ )
164
+
165
+
166
+ def _resume_argv(work_dir, args):
167
+ """The argv that resumes this run: what the user typed (paths made absolute by
168
+ recap_cli), plus the work dir and any environment-filled setting it did not spell out.
169
+ Values a --project binding filled in are re-derived from --project on resume."""
170
+ argv = list(args._argv)
171
+ explicit = args._explicit_options
172
+ bound = getattr(args, "_bound_from_project", frozenset())
173
+ if "--work-dir" not in explicit:
174
+ argv += ["--work-dir", str(work_dir)]
175
+ for dest, unset in _ENV_FILLED:
176
+ flag = "--" + dest.replace("_", "-")
177
+ value = getattr(args, dest)
178
+ if flag not in explicit and dest not in bound and value != unset:
179
+ argv += [flag, str(value)]
180
+ return argv
181
+
182
+
143
183
  def _write_run_manifest(work_dir, video, args):
144
- payload = _run_manifest_payload(video, args)
184
+ payload = {**_run_manifest_payload(video, args), "argv": _resume_argv(work_dir, args)}
145
185
  (work_dir / RUN_MANIFEST).write_text(
146
186
  json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
147
187
  )
@@ -151,14 +191,14 @@ def _write_run_manifest(work_dir, video, args):
151
191
  def _build_multi_source_records(videos, args):
152
192
  records = []
153
193
  for video in _coerce_videos(videos):
154
- identity = material_lib.file_identity(video)
194
+ identity = file_identity(video)
155
195
  records.append(
156
196
  {
157
197
  "source_path": str(video),
158
198
  "source_name": video.name,
159
199
  "source_video_identity": identity,
160
200
  "settings": _analysis_settings(args),
161
- "material_id": material_lib.material_id_for(video, identity),
201
+ "material_id": material_id_for(video, identity),
162
202
  }
163
203
  )
164
204
  records = material_lib.assign_source_ids(records)
@@ -188,7 +228,10 @@ def _multi_run_manifest_payload(videos, args, source_records):
188
228
 
189
229
 
190
230
  def _write_project_run_manifest(work_dir, videos, args, source_records):
191
- payload = _multi_run_manifest_payload(videos, args, source_records)
231
+ payload = {
232
+ **_multi_run_manifest_payload(videos, args, source_records),
233
+ "argv": _resume_argv(work_dir, args),
234
+ }
192
235
  (work_dir / RUN_MANIFEST).write_text(
193
236
  json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
194
237
  )
@@ -240,20 +283,41 @@ def _ffmpeg_present_but_cannot_burn():
240
283
  return not ffmpeg_has_subtitles_filter()
241
284
 
242
285
 
286
+ def _burn_subtitles_explicit(args):
287
+ """Burn was asked for, not inherited: --burn-subtitles, or a truthy BURN_SUBTITLES."""
288
+ if args.burn_subtitles is not None:
289
+ return args.burn_subtitles
290
+ return "BURN_SUBTITLES" in os.environ and env_bool("BURN_SUBTITLES", True)
291
+
292
+
243
293
  def _preflight_burn_subtitles(args):
244
- """Fail fast BEFORE any understanding/VLM/ASR/TTS spend when subtitle burn-in is on but
245
- this ffmpeg can't burn it. Without it the run only dies at the final assemble
246
- `-vf subtitles=` step — after the whole expensive pipeline has run. Dub renders through
247
- dub.py, which never burns subtitles, so it is exempt."""
294
+ """Settle subtitle burn-in BEFORE any understanding/VLM/ASR/TTS spend when this ffmpeg
295
+ lacks libass. An explicit request fails fast here, since otherwise the run only dies at
296
+ the final assemble `-vf subtitles=` step. The default burn only warns: assemble then
297
+ delivers the .srt sidecar and records `subtitle_burn_degraded` in visual_qc.json, which
298
+ final_qc.json carries in metadata.warnings. Dub renders through its own script, which
299
+ never burns subtitles, so it is exempt."""
248
300
  if args.edit_mode == "dub" or not _burn_subtitles_intended(args):
249
301
  return
250
- if _ffmpeg_present_but_cannot_burn():
302
+ if not _ffmpeg_present_but_cannot_burn():
303
+ return
304
+ doctor = f"python3 {shlex.quote(str(_entry('video-recap', 'doctor.py')))}"
305
+ if _burn_subtitles_explicit(args):
251
306
  raise SystemExit(
252
- "字幕烧录已开启,但当前 ffmpeg 不支持 subtitles/libass 滤镜,整条流程会跑到最后渲染才失败。\n"
253
- " 解决其一:(1) 安装带 libass 的 ffmpeg;(2) 加 --no-burn-subtitles 关闭烧录"
254
- "(仍输出 .srt 外挂字幕)。\n"
255
- f" 自检:python3 {shlex.quote(str(_entry('video-recap', 'doctor.py')))}"
307
+ "已显式要求烧录字幕(--burn-subtitles 或 BURN_SUBTITLES),但当前 ffmpeg 不支持"
308
+ " subtitles/libass 滤镜,整条流程会跑到最后渲染才失败。\n"
309
+ " 解决其一:(1) 安装带 libass 的 ffmpeg;(2) 去掉显式烧录要求,默认运行会改为输出"
310
+ " .srt 外挂字幕;(3) 加 --no-burn-subtitles。\n"
311
+ f" 自检:{doctor}"
256
312
  )
313
+ print(
314
+ "[video-recap] ⚠ 当前 ffmpeg 不支持 subtitles/libass 滤镜:本次成片不烧录字幕。"
315
+ "有字幕条目时改为在成片旁输出同名 .srt 外挂字幕(final_qc.json 的 metadata.warnings"
316
+ " 会记录 subtitle_burn_degraded);没有字幕条目的运行(如 source 音频模式且没有"
317
+ " user_subtitles.*)不输出 .srt,也不记警告。要烧录字幕请安装带 libass 的 ffmpeg;"
318
+ f"自检:{doctor}",
319
+ flush=True,
320
+ )
257
321
 
258
322
 
259
323
  def _entry(skill, script):
@@ -24,8 +24,6 @@ _LOCAL_ADOPTION_OPTIONS = (
24
24
  )
25
25
 
26
26
  _LOCAL_ADOPTION_CONFLICTS = _TTS_OPTIONS | frozenset({
27
- "--mimo-qc",
28
- "--mimo-qc-refresh",
29
27
  "--export-jianying",
30
28
  "--jianying-bundle-media",
31
29
  "--jianying-no-bundle-media",
@@ -106,7 +104,6 @@ def validate_local_adoption(parser, args):
106
104
  args.tts_provider = "auto"
107
105
  args.mimo_tts_voice = None
108
106
  args.voice_ref = None
109
- args.mimo_qc = "off"
110
107
 
111
108
 
112
109
  def load_local_assembly_evidence(work_dir):
@@ -215,8 +212,6 @@ def validate_audio_routing(parser, args):
215
212
  if conflicts:
216
213
  detail = ", ".join(conflicts)
217
214
  parser.error(f"--audio-mode {mode} cannot use TTS/strict narration options: {detail}")
218
- if args.mimo_qc != "off":
219
- parser.error(f"--audio-mode {mode} currently requires --mimo-qc off")
220
215
  if args.edit_mode == "cut" and stream != 0:
221
216
  parser.error("cut audio modes currently require --audio-stream-index 0")
222
217
  if args.export_jianying and stream != 0:
@@ -242,37 +237,3 @@ def reject_unsupported_subtitle_track(work_dir, args):
242
237
  "source-mix 当前不能绑定显式 subtitle_track.json;请使用新的 work_dir,"
243
238
  "或选择 adopted-packet-copy"
244
239
  )
245
-
246
-
247
- def begin_non_narration_qc(work_dir, args, write_stage):
248
- """Start a clean run-local QC ledger without reading or deleting old TTS."""
249
- (Path(work_dir) / "preflight_qc.json").unlink(missing_ok=True)
250
- return write_stage(
251
- work_dir,
252
- "pre_assemble",
253
- metadata={
254
- "audio_mode": args.audio_mode,
255
- "selected_audio_stream_index": args.audio_stream_index,
256
- "tts": "not_applicable",
257
- "narration_validation": "not_applicable",
258
- "narration_review": "not_applicable",
259
- "visual_overlays": "preserved_not_authored_by_this_run",
260
- },
261
- )
262
-
263
-
264
- def begin_local_adoption_qc(work_dir, write_stage):
265
- """Record that authoring was intentionally skipped for adopted local assets."""
266
- return write_stage(
267
- work_dir,
268
- "pre_assemble",
269
- metadata={
270
- "audio_mode": "narration",
271
- "selected_audio_stream_index": 0,
272
- "tts": "adopted_local_not_generated",
273
- "narration_validation": "not_run",
274
- "narration_review": "not_run",
275
- "semantic_validation": "video-assemble",
276
- "visual_overlays": "not_authored_not_present",
277
- },
278
- )
@@ -1,72 +1,13 @@
1
- """Write shift-left, MiMo, and final QC stage reports."""
1
+ """Write final_qc.json after render and enforce --require-final-qc."""
2
2
 
3
- import json
4
3
  from pathlib import Path
5
4
 
6
5
  import final_qc
7
- import mimo_qc
8
- import qc_contract
9
-
10
- ASSEMBLY_MANIFEST = "assembly_manifest.json"
11
- PREFLIGHT_QC = "preflight_qc.json"
12
-
13
-
14
- def _load_preflight_stage_reports(work_dir):
15
- path = Path(work_dir) / PREFLIGHT_QC
16
- if not path.exists():
17
- return {}
18
- return json.loads(path.read_text(encoding="utf-8"))["metadata"]["stages"]
19
-
20
-
21
- def _write_shift_left_stage_qc(work_dir, stage, metadata, findings=None):
22
- """Write/roll up local shift-left QC for one pipeline stage.
23
-
24
- This is a local contract artifact only: no MiMo/deep eval calls, no repair, and no
25
- credential persistence (qc_contract redacts every report it builds).
26
- """
27
- stage_report = qc_contract.build_report(
28
- artifact=PREFLIGHT_QC, stage=stage, findings=findings, metadata=metadata
29
- )
30
- stage_reports = _load_preflight_stage_reports(work_dir)
31
- stage_reports[stage] = stage_report
32
- top_metadata = dict(stage_report["metadata"])
33
- top_metadata["latest_stage"] = stage
34
- top_metadata["stages"] = stage_reports
35
- top_report = qc_contract.build_report(
36
- artifact=PREFLIGHT_QC,
37
- stage=stage,
38
- findings=[f for report in stage_reports.values() for f in report["findings"]],
39
- metadata=top_metadata,
40
- )
41
- (Path(work_dir) / PREFLIGHT_QC).write_text(
42
- json.dumps(top_report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
43
- )
44
- return top_report
45
-
46
-
47
- def _tts_qc_metadata(work_dir):
48
- """Called after video-voiceover exited 0, which always writes tts_meta.json."""
49
- work_dir = Path(work_dir)
50
- metadata = {"tts_meta": json.loads((work_dir / "tts_meta.json").read_text(encoding="utf-8"))}
51
- tts_dir = work_dir / "tts_segments"
52
- if tts_dir.is_dir():
53
- metadata["tts_segments"] = [
54
- p.relative_to(work_dir).as_posix() for p in sorted(tts_dir.iterdir()) if p.is_file()
55
- ]
56
- return metadata
57
-
58
-
59
- def _post_render_qc_metadata(work_dir, final_output):
60
- """Called after video-assemble exited 0, which always writes assembly_manifest.json."""
61
- manifest = Path(work_dir) / ASSEMBLY_MANIFEST
62
- return {
63
- "final_output": str(final_output),
64
- "assembly_manifest": json.loads(manifest.read_text(encoding="utf-8")),
65
- }
6
+ from lib import read_json_object
66
7
 
67
8
 
68
9
  def _write_final_qc_reports(work_dir, final_output):
69
- """Write report-only final QC artifacts after render.
10
+ """Write the report-only final_qc.json after render.
70
11
 
71
12
  final_qc.run converts ffprobe unavailability/failure into deterministic
72
13
  blockers; only unexpected schema/write errors propagate.
@@ -75,86 +16,47 @@ def _write_final_qc_reports(work_dir, final_output):
75
16
 
76
17
 
77
18
  def _print_final_qc_pointer(result):
78
- """Surface a report-only final_qc/golden_eval FAIL so the shift-left QC is
79
- not a silent no-op. Advisory only: it never changes the exit status."""
80
- problems = [
81
- f"{key} blocker_count={result[key].get('blocker_count', '?')}"
82
- for key in ("final_qc", "golden_eval")
83
- if result[key].get("ok") is False
84
- ]
85
- if problems:
19
+ """Surface a report-only final_qc FAIL so the final QC is not a silent
20
+ no-op. Advisory only: it never changes the exit status."""
21
+ summary = result["final_qc"]
22
+ if summary.get("ok") is False:
86
23
  print(
87
24
  "[video-recap] ⚠️ 最终 QC 未通过(仅报告,不阻断): "
88
- + "; ".join(problems)
89
- + ";详见 final_qc.json / golden_eval.json"
25
+ f"final_qc blocker_count={summary.get('blocker_count', '?')}"
26
+ ";详见 final_qc.json"
90
27
  )
91
28
 
92
29
 
93
30
  def _require_final_qc(result, work_dir):
94
- """Fail closed unless both final summaries are literal blocker-free passes."""
95
- paths = [Path(work_dir) / name for name in ("final_qc.json", "golden_eval.json")]
96
- print("[video-recap] 最终 QC 报告: " + "; ".join(map(str, paths)))
97
- invalid = []
98
- for name in ("final_qc", "golden_eval"):
99
- summary = result.get(name) if isinstance(result, dict) else None
100
- blockers = summary.get("blocker_count") if isinstance(summary, dict) else None
101
- if not isinstance(summary, dict) or summary.get("ok") is not True or \
102
- type(blockers) is not int or blockers != 0:
103
- invalid.append(name)
31
+ """Fail closed unless the final_qc summary is a literal blocker-free pass."""
32
+ print(f"[video-recap] 最终 QC 报告: {Path(work_dir) / 'final_qc.json'}")
33
+ summary = result.get("final_qc") if isinstance(result, dict) else None
34
+ blockers = summary.get("blocker_count") if isinstance(summary, dict) else None
35
+ invalid = not isinstance(summary, dict) or summary.get("ok") is not True or \
36
+ type(blockers) is not int or blockers != 0
104
37
  if invalid:
105
- raise SystemExit(
106
- "严格最终 QC 未通过或摘要格式无效: " + ", ".join(invalid)
107
- )
108
-
109
-
110
- def _mimo_qc_stage_enabled(args, stage):
111
- return (
112
- args.mimo_qc == "both"
113
- or (args.mimo_qc == "pre-assemble" and stage == "pre_assemble")
114
- or (args.mimo_qc == "post-render" and stage == "post_render")
115
- )
116
-
117
-
118
- def _prepare_mimo_qc(work_dir, args):
119
- """Remove an old advisory artifact when this run has MiMo QC disabled."""
120
- if args.mimo_qc == "off":
121
- mimo_qc.clear_report(work_dir)
122
-
123
-
124
- def _print_mimo_qc_pointer(result, stage):
125
- report = result["report"]
126
- metadata = report["metadata"]
127
- status = metadata["status"]
128
- stage_findings = [f for f in report["findings"] if f["stage"] == stage]
129
- if status in {"failed", "unavailable"}:
38
+ raise SystemExit("严格最终 QC 未通过或摘要格式无效: final_qc")
39
+
40
+
41
+ def _print_render_warnings(result, work_dir, final_output):
42
+ """Relay non-blocking render warnings (final_qc metadata.warnings) and the subtitle
43
+ sidecar, so a degraded run is never reported as a plain success. The sidecar comes from
44
+ assembly_manifest.json only when that manifest rendered `final_output` (dub never
45
+ writes one, so a stale manifest from an earlier run must not be quoted)."""
46
+ codes = (result.get("final_qc") or {}).get("warnings") or []
47
+ manifest = read_json_object(Path(work_dir) / "assembly_manifest.json") or {}
48
+ rendered = manifest.get("final_output")
49
+ same_render = isinstance(rendered, str) and \
50
+ Path(rendered).resolve() == Path(final_output).resolve()
51
+ sidecar = manifest.get("subtitle_sidecar") if same_render else None
52
+ if "subtitle_burn_degraded" in codes:
130
53
  print(
131
- f"[video-recap] ⚠ MiMo QC {stage}: {status} ({metadata['error']});建议性检查不可用,继续流水线"
54
+ "[video-recap] ⚠️ 字幕未烧录进画面(当前 ffmpeg 缺 libass):"
55
+ f"字幕以外挂文件交付 {sidecar};详见 final_qc.json 的 metadata.warnings"
132
56
  )
133
- return
134
- print(
135
- f"[video-recap] ℹ MiMo QC {stage}: {status}, {len(stage_findings)} 条建议;详见 {result['path']}"
136
- )
137
- for finding in stage_findings[:5]:
138
- print(f"[video-recap] - {finding['message']}")
139
-
57
+ elif sidecar:
58
+ print(f"[video-recap] 外挂字幕: {sidecar}")
59
+ for code in codes:
60
+ if code != "subtitle_burn_degraded":
61
+ print(f"[video-recap] ⚠️ 渲染警告: {code};详见 final_qc.json 的 metadata.warnings")
140
62
 
141
- def _run_mimo_qc_stage(work_dir, args, stage, *, final_output=None):
142
- """Run one selected advisory stage and never propagate a failure."""
143
- if not _mimo_qc_stage_enabled(args, stage):
144
- return None
145
- try:
146
- result = mimo_qc.run(
147
- work_dir,
148
- stage=stage,
149
- live=True,
150
- refresh=args.mimo_qc_refresh,
151
- final_output=final_output,
152
- )
153
- except Exception as exc:
154
- print(
155
- f"[video-recap] ⚠ MiMo QC {stage}: {type(exc).__name__};"
156
- "建议性检查失败,继续流水线"
157
- )
158
- return None
159
- _print_mimo_qc_pointer(result, stage)
160
- return result