@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
@@ -1,216 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Pair explicit picture and adopted audio assets without re-encoding either stream.
3
-
4
- This is asset pairing, not a claim of source reconstruction, correct editorial
5
- selection, speech alignment, or release approval. Existing assemble then consumes
6
- paired.mp4; its subtitle binding must be newly computed on that container/a:0.
7
- """
8
-
9
- import argparse
10
- from fractions import Fraction
11
- import json
12
- from pathlib import Path
13
- import subprocess
14
-
15
- from assemble_constants import SUPPORTED_PICTURE_CODECS
16
- from adoption.frozen_audio import probe_audio_packets, verify_adopted_audio
17
- from adoption.strict_inputs import (
18
- probe_json, require_declared_path, require_fields, require_integer, run_logged,
19
- without_digests, write_json_atomic,
20
- )
21
-
22
-
23
- _probe = probe_json
24
-
25
-
26
- def _asset(value, audio=False):
27
- value = without_digests(value, 'asset')
28
- require_fields(value, ['path', 'selected_stream'] if audio else ['path'], 'asset')
29
- path = require_declared_path(value, 'asset')
30
- if audio:
31
- return {'path': str(path),
32
- 'selected_stream': require_integer(value['selected_stream'], 'Audio selected_stream')}
33
- return {'path': str(path)}
34
-
35
-
36
- def _time(value):
37
- if value in (None, 'N/A'):
38
- raise ValueError('Missing media timing')
39
- try:
40
- return Fraction(str(value))
41
- except (ValueError, ZeroDivisionError) as exc:
42
- raise ValueError('Invalid media timing') from exc
43
-
44
-
45
- def probe_picture(path):
46
- """Actual full CFR presentation clock plus codec-order packet sizes and timestamps."""
47
- data = _probe(path, '-select_streams', 'v:0', '-show_streams', '-show_packets',
48
- '-show_format', '-show_entries',
49
- 'format=format_name:stream=codec_name,profile,level,width,height,pix_fmt,'
50
- 'sample_aspect_ratio,field_order,color_range,color_space,color_transfer,'
51
- 'color_primaries,chroma_location,time_base,start_pts,duration_ts,avg_frame_rate'
52
- ':packet=pts,dts,duration,size,side_data_list')
53
- streams = data.get('streams', [])
54
- if len(streams) != 1 or streams[0].get('codec_name') not in SUPPORTED_PICTURE_CODECS:
55
- raise ValueError('Pairing requires one selected H264/HEVC picture stream')
56
- if 'mp4' not in data.get('format', {}).get('format_name', '').split(','):
57
- raise ValueError('Pairing currently requires MP4-family picture container')
58
- v = streams[0]
59
- fps, tb = _time(v.get('avg_frame_rate')), _time(v.get('time_base'))
60
- if not 1 <= fps <= 120 or tb <= 0 or v.get('start_pts') != 0:
61
- raise ValueError('Picture requires positive CFR and a known zero start')
62
- duration = _time(v.get('duration_ts')) * tb
63
- frames = _probe(path, '-select_streams', 'v:0', '-show_frames',
64
- '-show_entries', 'frame=pts')['frames']
65
- pts = [_time(frame.get('pts')) * tb for frame in frames]
66
- if not pts or any(t != Fraction(i, fps) for i, t in enumerate(pts)) or duration != len(pts) / fps:
67
- raise ValueError('Picture requires complete zero-origin CFR frame clock and exact duration')
68
- decoder_keys = ['codec_name', 'profile', 'level', 'width', 'height', 'pix_fmt',
69
- 'sample_aspect_ratio', 'field_order', 'color_range', 'color_space',
70
- 'color_transfer', 'color_primaries', 'chroma_location']
71
- packets = []
72
- for packet in data.get('packets', []):
73
- if packet.get('size') is None:
74
- raise ValueError('Picture packet missing size')
75
- ticks = {key: str(_time(packet.get(key)) * tb) for key in ['pts', 'dts', 'duration']}
76
- if _time(ticks['duration']) <= 0:
77
- raise ValueError('Picture packet has invalid duration')
78
- packets.append({**ticks, 'size': int(packet['size']),
79
- 'side_data_list': packet.get('side_data_list', [])})
80
- if len(packets) != len(pts):
81
- raise ValueError('Picture packet/frame counts disagree')
82
- return {'decoder': {key: v.get(key) for key in decoder_keys}, 'packets': packets,
83
- 'frame_pts': [str(t) for t in pts], 'frame_count': len(pts), 'fps': str(fps),
84
- 'duration': str(duration), 'start': '0'}
85
-
86
-
87
- def validate_aac_packet_interval(audio):
88
- """Validate exact AAC packet continuity against the stream-header interval."""
89
- if audio['codec'] != 'aac':
90
- raise ValueError('Pairing requires AAC adopted audio')
91
- packets = audio['packets']
92
- rate = audio['sample_rate']
93
- if type(rate) is not int or rate <= 0 or len(packets) < 2:
94
- raise ValueError('AAC packet timing is incomplete')
95
- nominal = _time(packets[0]['duration'])
96
- # Accept known AAC frame sample counts, never an arbitrary huge duration.
97
- if nominal * rate not in {960, 1024, 1920, 2048}:
98
- raise ValueError('Unsupported AAC nominal packet duration')
99
- for i, packet in enumerate(packets):
100
- d = _time(packet['duration'])
101
- if d <= 0 or d > nominal or (i < len(packets) - 1 and d != nominal):
102
- raise ValueError('Nonuniform or invalid AAC packet duration')
103
- p, t = _time(packet['pts']), _time(packet['dts'])
104
- if p != t:
105
- raise ValueError('Unsupported AAC PTS/DTS difference')
106
- if i and p != _time(packets[i-1]['pts']) + _time(packets[i-1]['duration']):
107
- raise ValueError('AAC packet clock has gaps or overlaps')
108
- audio_start, audio_duration = _time(audio['start_time']), _time(audio['duration'])
109
- if audio_duration <= 0:
110
- raise ValueError('Invalid audio interval')
111
- audio_end = audio_start + audio_duration
112
- first_pts = _time(packets[0]['pts'])
113
- last_end = _time(packets[-1]['pts']) + _time(packets[-1]['duration'])
114
- if (not 0 <= audio_start - first_pts <= nominal
115
- or abs(last_end - audio_end) > Fraction(1, rate)):
116
- raise ValueError('AAC packet clock does not match stream interval')
117
- return audio_start, audio_end, nominal
118
-
119
-
120
- def validate_pair_timing(picture, audio):
121
- """Narrow AAC/CFR compatibility; not a perceptual synchronization verdict."""
122
- audio_start, audio_end, nominal = validate_aac_packet_interval(audio)
123
- tolerance = max(1 / Fraction(picture['fps']), nominal)
124
- end_delta = audio_end - Fraction(picture['duration'])
125
- if abs(audio_start) > tolerance or abs(end_delta) > tolerance:
126
- raise ValueError('Picture/audio interval mismatch; no implicit trim, padding, offset or retime')
127
- return {'start_delta': str(audio_start),
128
- 'end_delta': str(end_delta),
129
- 'tolerance': str(tolerance), 'nominal_aac_packet': str(nominal)}
130
-
131
-
132
- def _run_mux(command, directory):
133
- run_logged(command, directory, 'mux', timeout=600)
134
-
135
-
136
- def _verify_output(path):
137
- data = _probe(path, '-show_streams')
138
- if [stream.get('codec_type') for stream in data['streams']] != ['video', 'audio']:
139
- raise ValueError('Paired output must contain only v:0 and a:0')
140
- result = subprocess.run(['ffmpeg', '-v', 'error', '-xerror', '-threads', '2', '-i', str(path),
141
- '-map', '0:v:0', '-map', '0:a:0', '-f', 'null', '-'],
142
- capture_output=True, text=True, timeout=600)
143
- if result.returncode or result.stderr.strip():
144
- raise ValueError('Paired output full decode failed')
145
-
146
-
147
- def run_pair(plan_path, output_dir, *, plan_only=False):
148
- plan_path, directory = Path(plan_path).resolve(), Path(output_dir).resolve()
149
- directory.mkdir(parents=True, exist_ok=False)
150
- report_path = directory / 'pair_run.json'
151
- staged, output = directory / 'paired.rendering.mp4', directory / 'paired.mp4'
152
- report = {'artifact': 'media_pair_run', 'schema_version': 1, 'status': 'PREPARING',
153
- 'direct_listening': 'NOT_CHECKED', 'normal_speed_review': 'NOT_CHECKED',
154
- 'release_approved': False}
155
- write_json_atomic(report_path, report)
156
- try:
157
- plan = json.loads(plan_path.read_bytes())
158
- require_fields(plan, ['artifact', 'schema_version', 'picture', 'audio'], 'media_pair plan')
159
- if plan['artifact'] != 'media_pair' or type(plan['schema_version']) is not int or plan['schema_version'] != 1:
160
- raise ValueError('Unsupported media_pair schema')
161
- picture, audio = _asset(plan['picture']), _asset(plan['audio'], audio=True)
162
- report.update(plan={'path': str(plan_path)}, inputs={'picture': picture, 'audio': audio})
163
- video_facts = probe_picture(picture['path'])
164
- audio_facts = probe_audio_packets(audio['path'], audio['selected_stream'])
165
- report['timing'] = validate_pair_timing(video_facts, audio_facts)
166
- report['picture'] = {k: v for k, v in video_facts.items() if k not in ['packets', 'frame_pts']}
167
- report['audio'] = {'input_stream': audio['selected_stream'], 'output_stream': 0,
168
- 'packet_count': audio_facts['packet_count']}
169
- if plan_only:
170
- report['status'] = 'PLANNED'
171
- write_json_atomic(report_path, report)
172
- return report
173
- command = ['ffmpeg', '-nostdin', '-v', 'error', '-n', '-copyts',
174
- '-i', picture['path'], '-i', audio['path'], '-map', '0:v:0',
175
- '-map', f"1:a:{audio['selected_stream']}", '-c', 'copy',
176
- '-movie_timescale', str(audio_facts['sample_rate']),
177
- '-movflags', '+faststart', str(staged)]
178
- _run_mux(command, directory)
179
- if probe_picture(staged) != video_facts:
180
- raise ValueError('Paired picture packets, decoder, geometry/color or full frame clock changed')
181
- proof = verify_adopted_audio(audio['path'], staged, audio['selected_stream'], 0)
182
- output_timing = validate_pair_timing(video_facts, proof['output'])
183
- if output_timing != report['timing']:
184
- raise ValueError('Output audio presentation interval changed')
185
- report['output_timing'] = output_timing
186
- _verify_output(staged)
187
- write_json_atomic(directory / 'picture_identity.json', video_facts)
188
- write_json_atomic(directory / 'adopted_audio_identity.json', proof)
189
- report['output'] = {'path': str(output), 'full_decode': 'PASS',
190
- 'picture_identity': 'EXACT', 'audio_packet_identity': 'EXACT'}
191
- staged.rename(output)
192
- report['status'] = 'PAIR_RENDERED'
193
- write_json_atomic(report_path, report)
194
- return report
195
- except Exception as exc:
196
- # A partial unique run is evidence, never a final/current asset. Keep logs.
197
- staged.unlink(missing_ok=True)
198
- output.unlink(missing_ok=True)
199
- report.pop('output', None)
200
- report.update(status='FAILED', error=f'{type(exc).__name__}: {exc}')
201
- write_json_atomic(report_path, report)
202
- raise
203
-
204
-
205
- def main():
206
- parser = argparse.ArgumentParser(description=__doc__)
207
- parser.add_argument('plan')
208
- parser.add_argument('--output-dir', required=True)
209
- parser.add_argument('--plan-only', action='store_true')
210
- args = parser.parse_args()
211
- report = run_pair(args.plan, args.output_dir, plan_only=args.plan_only)
212
- print(json.dumps(report, ensure_ascii=False, indent=2))
213
-
214
-
215
- if __name__ == '__main__':
216
- main()
@@ -1,23 +0,0 @@
1
- # 前置 QC 数据契约
2
-
3
- `final_qc.json`、`golden_eval.json`、`mimo_qc.json` 与 `preflight_qc.json` 共用一套最小结构,由本技能的 `scripts/qc_contract.py` 实现。
4
-
5
- ## 字段契约
6
-
7
- - `schema_version`:整数 `1`。
8
- - `artifact`:只能是 `final_qc.json`、`golden_eval.json`、`mimo_qc.json` 或 `preflight_qc.json`。
9
- - `stage`:只能是 `pre_cut`、`post_cut`、`pre_tts`、`post_tts`、`pre_assemble`、`post_render`、`golden`。
10
- - 不得使用 `pre_voiceover`、`post_voiceover`、`pre_export`、`post_export`、`final`、`golden_eval` 或 `mimo_qc` 作为阶段值。
11
- - `mimo_qc.json` 是产物名;MiMo finding 必须挂在 `post_tts`、`pre_assemble` 等真实阶段上。
12
- - `findings[]`:每项必须包含 `finding_id`、`stage`、`severity`、`blocking`、`deterministic`、`confidence`、`rule_id`、`decision_reason`、`location`、`evidence`、`sample_policy`、`model_used` 与 `next_action`。
13
- - `sample_policy`:至少包含 `type`;其值只能是 `all`、`deterministic`、`sampled`、`semantic` 或 `aesthetic`。
14
- - `location.timecode` 与 `location.source_span` 必须存在;剪辑前阶段可以把任一字段设为 `null`。
15
- - 为兼容与诊断,当前辅助函数还可能写出 `id`、`category`、`code`、`message`、`source` 与 `objective_corroboration`。
16
-
17
- ## 阻断语义
18
-
19
- 只有确定性的客观规则可以阻断,例如产物缺失或过期、时长或媒体流错误、字幕与 TTS 放置错误、数据结构无效。
20
-
21
- MiMo 语义/审美 finding 以及其他所有非确定性 finding **始终只给建议,不能阻断**。若存在客观佐证,必须由确定性生产者单独写成确定性 finding;它不能把主观模型观察升级成 blocker。运行时不存在 allow-list 或规则表逃生口。
22
-
23
- `qc_contract.py` 只负责数据结构:不调用 MiMo、不连接流水线,也不自动修复。独立的 `mimo_qc.py` 只有在显式开启时,才会在 `pre_assemble` 和/或 `post_render` 各发起一次实时请求;它保持 fail-open,且不落盘凭证或抽帧 base64。所有报告构建器都通过 `qc_contract.redact_secrets` 处理元数据与证据:脱敏疑似密钥的键值,并从 URL 移除 userinfo、query 与 fragment,同时保留有用的 host/path 上下文。
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Public API and CLI entrypoint for advisory MiMo multimodal QC."""
3
-
4
- import qc.mimo_runner as mimo_qc_runner
5
- from qc.mimo_evidence import collect_evidence, safe_mimo_config
6
- from qc.mimo_observations import normalize_observations
7
- from qc.mimo_payload import build_payload
8
- from qc.mimo_client import mimo_qc_api_call
9
-
10
- clear_report = mimo_qc_runner.clear_report
11
-
12
-
13
- def run(work_dir, **kwargs):
14
- """mimo_qc_runner.run with the real MiMo transport bound at call time."""
15
- return mimo_qc_runner.run(work_dir, api_call=mimo_qc_api_call, **kwargs)
16
-
17
-
18
- def main(argv=None):
19
- return mimo_qc_runner.main(argv, run_callable=run)
20
-
21
- __all__ = [
22
- "build_payload",
23
- "clear_report",
24
- "collect_evidence",
25
- "main",
26
- "mimo_qc_api_call",
27
- "normalize_observations",
28
- "run",
29
- "safe_mimo_config",
30
- ]
31
-
32
- if __name__ == "__main__":
33
- raise SystemExit(main())
@@ -1 +0,0 @@
1
- """MiMo QC subpackage."""
@@ -1,13 +0,0 @@
1
- """Load this skill's MiMo client without depending on the ambient ``lib`` module."""
2
-
3
- import importlib.util
4
- from pathlib import Path
5
-
6
- _SPEC = importlib.util.spec_from_file_location(
7
- "video_recap_mimo_qc_lib", Path(__file__).resolve().parents[1] / "lib.py"
8
- )
9
- _LIB = importlib.util.module_from_spec(_SPEC)
10
- _SPEC.loader.exec_module(_LIB)
11
-
12
- DEFAULT_CONFIG = _LIB.CONFIG
13
- mimo_qc_api_call = _LIB.mimo_qc_api_call
@@ -1,9 +0,0 @@
1
- """Shared constants for the self-contained MiMo QC adapter."""
2
-
3
- ARTIFACT_NAME = "mimo_qc.json"
4
- DEFAULT_STAGE = "pre_assemble"
5
- MAX_OBSERVATIONS = 12
6
- MAX_MESSAGE_CHARS = 800
7
- MAX_FRAMES = 6
8
- MAX_FRAME_DIMENSION = 768
9
- FRAME_SAMPLER_VERSION = 2
@@ -1,287 +0,0 @@
1
- """Collect and redact local evidence for MiMo QC."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- from pathlib import Path
8
- from typing import Any
9
- from collections.abc import Mapping, Sequence
10
-
11
- import qc_contract
12
- from qc.mimo_client import DEFAULT_CONFIG
13
-
14
- _JSON_ARTIFACTS = (
15
- "narration.json",
16
- "visual_overlays.json",
17
- "clip_plan_validated.json",
18
- "clip_plan.json",
19
- "assembly_manifest.json",
20
- "tts_meta.json",
21
- )
22
-
23
- _SOURCE_ASR_ARTIFACTS = (
24
- "asr_clean.json",
25
- "asr.json",
26
- "asr_result.json",
27
- "asr_segments.json",
28
- )
29
-
30
- _GENERATED_SUBTITLE_ARTIFACTS = (
31
- "subtitles.json",
32
- "subtitle.json",
33
- "subtitles.srt",
34
- "subtitle.srt",
35
- "subtitles.vtt",
36
- "subtitle.vtt",
37
- "output.srt",
38
- "output.vtt",
39
- )
40
-
41
- _OPTIONAL_VISUAL_METADATA = (
42
- "sampled_frames.json",
43
- "frame_samples.json",
44
- "storyboard.json",
45
- "storyboard_meta.json",
46
- "frames_manifest.json",
47
- )
48
-
49
-
50
- def _load_json(path: Path) -> Any:
51
- return json.loads(path.read_text(encoding="utf-8"))
52
-
53
-
54
- def _read_text_sample(path: Path, *, max_chars: int = 4000) -> dict[str, Any]:
55
- text = path.read_text(encoding="utf-8", errors="replace")
56
- return {
57
- "kind": "text",
58
- "bytes": path.stat().st_size,
59
- "truncated": len(text) > max_chars,
60
- "sample": text[:max_chars],
61
- }
62
-
63
-
64
- def _summarize(
65
- value: Any, *, max_items: int = 8, max_string: int = 700, depth: int = 0
66
- ) -> Any:
67
- """Keep request/report evidence bounded while retaining useful structure."""
68
- if depth >= 4:
69
- return {"type": type(value).__name__, "omitted": True}
70
- if isinstance(value, Mapping):
71
- out = {
72
- str(key): _summarize(
73
- item, max_items=max_items, max_string=max_string, depth=depth + 1
74
- )
75
- for key, item in list(value.items())[:max_items]
76
- }
77
- if len(value) > max_items:
78
- out["_omitted_keys"] = len(value) - max_items
79
- return out
80
- if isinstance(value, list):
81
- return {
82
- "count": len(value),
83
- "items": [
84
- _summarize(
85
- item, max_items=max_items, max_string=max_string, depth=depth + 1
86
- )
87
- for item in value[:max_items]
88
- ],
89
- "omitted": max(0, len(value) - max_items),
90
- }
91
- if isinstance(value, str) and len(value) > max_string:
92
- return {"text": value[:max_string], "truncated": True, "chars": len(value)}
93
- return value
94
-
95
-
96
- def _collect_file(work_dir: Path, name: str) -> dict[str, Any] | None:
97
- path = work_dir / name
98
- if not path.is_file():
99
- return None
100
- if path.suffix.lower() == ".json":
101
- kind, summary = "json", _summarize(_load_json(path))
102
- else:
103
- kind, summary = "text", _summarize(_read_text_sample(path))
104
- stat = path.stat()
105
- return {
106
- "path": name,
107
- "kind": kind,
108
- "bytes": stat.st_size,
109
- "mtime_ns": stat.st_mtime_ns,
110
- "summary": summary,
111
- }
112
-
113
-
114
- def _first_existing(work_dir: Path, names: Sequence[str]) -> str | None:
115
- return next((name for name in names if (work_dir / name).is_file()), None)
116
-
117
-
118
- def _collect_multi_source_asr(work_dir: Path) -> dict[str, Any]:
119
- """Collect per-source ASR when a multi-source project has no root transcript."""
120
- manifest_path = work_dir / "multi_source_manifest.json"
121
- if not manifest_path.is_file():
122
- return {}
123
- collected = {}
124
- for source in _load_json(manifest_path)["sources"]:
125
- relative_dir = source["source_work_dir"] # recap writes sources/<source_id>
126
- name = _first_existing(work_dir / relative_dir, _SOURCE_ASR_ARTIFACTS)
127
- if name is not None:
128
- collected[source["source_id"]] = _collect_file(work_dir, f"{relative_dir}/{name}")
129
- return collected
130
-
131
-
132
- def _resolve_candidate(work_dir: Path, candidate: str | Path) -> Path:
133
- path = Path(candidate)
134
- return path if path.is_absolute() else work_dir / path
135
-
136
-
137
- def _final_output_path(
138
- work_dir: Path, final_output: str | Path | None
139
- ) -> tuple[Path, str] | None:
140
- """(path, display) of the final output: the caller's, else assembly_manifest.final_output
141
- (video-assemble always writes it); None before the assembler has run."""
142
- if final_output is None:
143
- manifest_path = work_dir / "assembly_manifest.json"
144
- if not manifest_path.is_file():
145
- return None
146
- final_output = _load_json(manifest_path)["final_output"]
147
- return _resolve_candidate(work_dir, final_output), str(final_output)
148
-
149
-
150
- def _final_output_metadata(
151
- work_dir: Path, final_output: str | Path | None = None
152
- ) -> dict[str, Any]:
153
- resolved = _final_output_path(work_dir, final_output)
154
- if resolved is None:
155
- return {"candidates": []}
156
- path, display = resolved
157
- item: dict[str, Any] = {"path": display, "exists": path.is_file()}
158
- if item["exists"]:
159
- stat = path.stat()
160
- item.update({"bytes": stat.st_size, "mtime_ns": stat.st_mtime_ns})
161
- return {"candidates": [item]}
162
-
163
-
164
- def _existing_final_output(
165
- work_dir: Path, final_output: str | Path | None
166
- ) -> Path | None:
167
- resolved = _final_output_path(work_dir, final_output)
168
- return resolved[0] if resolved is not None and resolved[0].is_file() else None
169
-
170
-
171
- def collect_evidence(
172
- work_dir: str | Path, *, final_output: str | Path | None = None
173
- ) -> dict[str, Any]:
174
- """Collect lightweight, secret-scrubbed evidence from one work directory."""
175
- root = Path(work_dir)
176
- artifacts: dict[str, Any] = {}
177
- preferred_plan = _first_existing(
178
- root, ("clip_plan_validated.json", "clip_plan.json")
179
- )
180
- for name in _JSON_ARTIFACTS:
181
- if (
182
- name in {"clip_plan_validated.json", "clip_plan.json"}
183
- and name != preferred_plan
184
- ):
185
- continue
186
- item = _collect_file(root, name)
187
- if item is not None:
188
- artifacts[name] = item
189
- evidence = {
190
- # Display only; excluded from cache_input so moving the work directory is a cache hit.
191
- "work_dir": str(root),
192
- "artifacts": artifacts,
193
- "source_asr": {
194
- name: item
195
- for name in _SOURCE_ASR_ARTIFACTS
196
- if (item := _collect_file(root, name)) is not None
197
- },
198
- "generated_subtitles": {
199
- name: item
200
- for name in _GENERATED_SUBTITLE_ARTIFACTS
201
- if (item := _collect_file(root, name)) is not None
202
- },
203
- "visual_metadata": {
204
- name: item
205
- for name in _OPTIONAL_VISUAL_METADATA
206
- if (item := _collect_file(root, name)) is not None
207
- },
208
- "final_output": _final_output_metadata(root, final_output),
209
- }
210
- if not evidence["source_asr"]:
211
- evidence["source_asr"] = _collect_multi_source_asr(root)
212
- # The evidence goes into the MiMo request as well as the persisted report, so it is
213
- # redacted here, before either.
214
- return qc_contract.redact_secrets(evidence)
215
-
216
-
217
- def _cache_file_group(group: Mapping[str, Any]) -> dict[str, Any]:
218
- return {
219
- name: {key: item[key] for key in ("kind", "bytes", "mtime_ns")}
220
- for name, item in sorted(group.items())
221
- }
222
-
223
-
224
- def _cache_evidence(evidence: Mapping[str, Any]) -> dict[str, Any]:
225
- """The per-file identities ({kind, bytes, mtime_ns}) a cached stage report was built
226
- from; work_dir is excluded so moving the directory is still a cache hit."""
227
- return {
228
- "artifacts": _cache_file_group(evidence["artifacts"]),
229
- "source_asr": _cache_file_group(evidence["source_asr"]),
230
- "generated_subtitles": _cache_file_group(evidence["generated_subtitles"]),
231
- "visual_metadata": _cache_file_group(evidence["visual_metadata"]),
232
- "final_output": [
233
- {key: item[key] for key in ("exists", "bytes", "mtime_ns") if key in item}
234
- for item in evidence["final_output"]["candidates"]
235
- ],
236
- }
237
-
238
-
239
- def _effective_config(config: Mapping[str, Any] | None = None) -> dict[str, Any]:
240
- source = dict(DEFAULT_CONFIG)
241
- if config:
242
- source.update(dict(config))
243
- if not config.get("mimo_qc_model"):
244
- source["mimo_qc_model"] = (
245
- config.get("mimo_video_model")
246
- or config.get("mimo_model")
247
- or source["mimo_qc_model"]
248
- )
249
- # MIMO_QC_MODEL is intentionally read at call time for embedded/CLI tests and
250
- # long-running agent processes whose environment may be adjusted between runs.
251
- if os.environ.get("MIMO_QC_MODEL") and not (config and config.get("mimo_qc_model")):
252
- source["mimo_qc_model"] = os.environ["MIMO_QC_MODEL"]
253
- return source
254
-
255
-
256
- def safe_mimo_config(config: Mapping[str, Any] | None = None) -> dict[str, Any]:
257
- """Return non-secret settings suitable for persisted report metadata."""
258
- source = _effective_config(config)
259
- keep = (
260
- "api_provider",
261
- "mimo_api_url",
262
- "mimo_api_url_source",
263
- "mimo_video_api_url",
264
- "mimo_video_api_url_source",
265
- "mimo_qc_model",
266
- "mimo_qc_model_source",
267
- "mimo_model",
268
- "mimo_model_source",
269
- "mimo_video_model",
270
- "mimo_video_model_source",
271
- "mimo_disable_thinking",
272
- "mimo_disable_thinking_source",
273
- "mimo_media_resolution",
274
- "mimo_media_resolution_source",
275
- )
276
- safe = {key: source[key] for key in keep}
277
- safe["model"] = (
278
- source["mimo_qc_model"] or source["mimo_video_model"] or source["mimo_model"]
279
- )
280
- key_present = bool(
281
- source.get("mimo_video_api_key")
282
- or source.get("mimo_api_key")
283
- or source.get("api_key")
284
- )
285
- safe = qc_contract.redact_secrets(safe)
286
- safe["key_present"] = key_present
287
- return safe