@oh-story/dsh 0.1.12 → 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
@@ -19,7 +19,6 @@ VALID_STATUSES = {
19
19
  "FAILED_AUDIO_EXTRACTION",
20
20
  "FAILED_PROVIDER",
21
21
  "EMPTY_UNKNOWN",
22
- "LEGACY_UNVERIFIED",
23
22
  }
24
23
  _TOP_KEYS = {
25
24
  "schema_version", "status", "source_video", "audio", "asr_result",
@@ -63,10 +62,8 @@ def load_glossary_names(work_dir):
63
62
  )
64
63
 
65
64
 
66
- def _glossary_binding(work_dir, *, legacy=False):
67
- """The glossary names that could have corrected this transcription (legacy: unknown)."""
68
- if legacy:
69
- return {"names": None, "name_count": None}
65
+ def _glossary_binding(work_dir):
66
+ """The glossary names that could have corrected this transcription."""
70
67
  names = load_glossary_names(work_dir)
71
68
  return {"names": names, "name_count": len(names)}
72
69
 
@@ -87,12 +84,12 @@ def _atomic_json_write(path, payload):
87
84
  pass
88
85
 
89
86
 
90
- def _window_evidence(observed_segments, final_segments, legacy):
87
+ def _window_evidence(observed_segments, final_segments):
91
88
  observed_segments = observed_segments or []
92
89
  windows = []
93
90
  for index, final in enumerate(final_segments or []):
94
91
  observed = observed_segments[index] if index < len(observed_segments) else None
95
- observed_text = None if legacy or observed is None else str(observed.get("text") or "")
92
+ observed_text = None if observed is None else str(observed.get("text") or "")
96
93
  final_text = str(final.get("text") or "")
97
94
  windows.append({
98
95
  "index": index,
@@ -111,20 +108,19 @@ def write_asr_timing_evidence(
111
108
  audio_path=None,
112
109
  ):
113
110
  """Write a sidecar recording which source/audio/result files it describes, without
114
- changing the legacy result."""
111
+ changing the result itself."""
115
112
  if status not in VALID_STATUSES:
116
113
  raise ValueError(f"unknown ASR timing evidence status: {status}")
117
114
  work_dir = Path(work_dir)
118
- legacy = status == "LEGACY_UNVERIFIED"
119
115
  payload = {
120
116
  "schema_version": SCHEMA_VERSION,
121
117
  "status": status,
122
118
  "source_video": _identity(video_path),
123
119
  "audio": _identity(audio_path) if audio_path else None,
124
120
  "asr_result": _identity(work_dir / "asr_result.json"),
125
- "glossary": _glossary_binding(work_dir, legacy=legacy),
121
+ "glossary": _glossary_binding(work_dir),
126
122
  "precision": dict(_PRECISION),
127
- "windows": _window_evidence(observed_segments, final_segments, legacy),
123
+ "windows": _window_evidence(observed_segments, final_segments),
128
124
  }
129
125
  path = work_dir / EVIDENCE_FILENAME
130
126
  _atomic_json_write(path, payload)
@@ -146,11 +142,9 @@ def _is_identity(value):
146
142
  )
147
143
 
148
144
 
149
- def _valid_glossary(payload, work_dir, legacy):
145
+ def _valid_glossary(payload, work_dir):
150
146
  if not isinstance(payload, dict) or set(payload) != _GLOSSARY_KEYS:
151
147
  return False
152
- if legacy:
153
- return payload == _glossary_binding(work_dir, legacy=True)
154
148
  return payload == _glossary_binding(work_dir) and _is_int(payload.get("name_count"))
155
149
 
156
150
 
@@ -162,8 +156,8 @@ def _valid_status_relationships(status, result, audio):
162
156
  return bool(result) and not any(texts) and _is_identity(audio)
163
157
  if status == "UNAVAILABLE_NO_DURATION":
164
158
  return result == [] and _is_identity(audio)
165
- if status in {"EXPLICITLY_SKIPPED", "UNAVAILABLE_NO_KEY", "LEGACY_UNVERIFIED"}:
166
- return audio is None and (status == "LEGACY_UNVERIFIED" or result == [])
159
+ if status in {"EXPLICITLY_SKIPPED", "UNAVAILABLE_NO_KEY"}:
160
+ return audio is None and result == []
167
161
  return False
168
162
 
169
163
 
@@ -199,8 +193,7 @@ def validate_asr_timing_evidence(evidence_path, video_path, asr_result_path):
199
193
  return False
200
194
  if not _valid_status_relationships(status, result, audio):
201
195
  return False
202
- legacy = status == "LEGACY_UNVERIFIED"
203
- if not _valid_glossary(evidence.get("glossary"), evidence_path.parent, legacy):
196
+ if not _valid_glossary(evidence.get("glossary"), evidence_path.parent):
204
197
  return False
205
198
 
206
199
  if audio is not None:
@@ -240,10 +233,7 @@ def validate_asr_timing_evidence(evidence_path, video_path, asr_result_path):
240
233
  or window.get("text_availability") != ("AVAILABLE" if text else "UNAVAILABLE_UNKNOWN")
241
234
  ):
242
235
  return False
243
- if legacy:
244
- if observed is not None or modified is not None:
245
- return False
246
- elif (
236
+ if (
247
237
  not isinstance(observed, str) or not isinstance(modified, bool)
248
238
  or modified != (observed != text)
249
239
  ):
@@ -265,6 +255,7 @@ def asr_evidence_summary_for_brief(work_dir, video_path):
265
255
  if valid:
266
256
  payload = json.loads(evidence_path.read_text(encoding="utf-8"))
267
257
  return {"status": payload["status"],
268
- "glossary_modifications": None if payload["status"] == "LEGACY_UNVERIFIED"
269
- else sum(w["glossary_modified"] is True for w in payload["windows"])}
258
+ "glossary_modifications": sum(
259
+ w["glossary_modified"] is True for w in payload["windows"]
260
+ )}
270
261
  return {"status": "MISSING_OR_STALE", "glossary_modifications": None}
@@ -9,10 +9,12 @@ from briefing.context import (
9
9
  _format_asr_chunks_for_brief,
10
10
  _format_background_research,
11
11
  _format_consolidation,
12
+ _format_moderation_refusals,
12
13
  _format_substrate_warning,
13
14
  _format_timeline_fusion_for_brief,
14
15
  _load_background_research,
15
16
  _load_consolidation,
17
+ _scene_number,
16
18
  _write_json_artifact,
17
19
  assess_understanding_substrate,
18
20
  )
@@ -28,7 +30,6 @@ from briefing.timeline import (
28
30
  _load_cut_output_spans_for_brief,
29
31
  _parse_target_seconds,
30
32
  _remap_brief_evidence_to_output_timeline,
31
- _write_deslop_qc_requirements,
32
33
  )
33
34
  from timeline_fusion import (
34
35
  _build_timeline_fusion,
@@ -36,6 +37,24 @@ from timeline_fusion import (
36
37
  _scene_asr_lines,
37
38
  )
38
39
 
40
+
41
+ def _duration_label(seconds):
42
+ """'45s' / '2min' / '1m42s' / '2h' / '2h01m' / '2h01m05s', rounded to whole seconds;
43
+ a unit is dropped only when it and everything below it are zero.
44
+
45
+ A minute-rounded label hid the real length: a 90 s target read '~2min'."""
46
+ whole = int(seconds + 0.5)
47
+ if whole < 60:
48
+ return f"{whole}s"
49
+ minutes, secs = divmod(whole, 60)
50
+ if minutes < 60:
51
+ return f"{minutes}min" if secs == 0 else f"{minutes}m{secs:02d}s"
52
+ hours, minutes = divmod(minutes, 60)
53
+ if minutes == 0 and secs == 0:
54
+ return f"{hours}h"
55
+ return f"{hours}h{minutes:02d}m" if secs == 0 else f"{hours}h{minutes:02d}m{secs:02d}s"
56
+
57
+
39
58
  def build_agent_brief(
40
59
  scenes_analysis,
41
60
  asr_result,
@@ -87,16 +106,8 @@ def build_agent_brief(
87
106
  beat_count_phrase = (
88
107
  f"at most ~{target_count}" if thin_substrate else f"roughly {target_count}"
89
108
  )
90
- output_label = (
91
- f"{output_seconds / 60:.0f}min"
92
- if output_seconds >= 60
93
- else f"{output_seconds:.0f}s"
94
- )
95
- source_label = (
96
- f"{video_duration / 60:.0f}min"
97
- if video_duration >= 60
98
- else f"{video_duration:.0f}s"
99
- )
109
+ output_label = _duration_label(output_seconds)
110
+ source_label = _duration_label(video_duration)
100
111
 
101
112
  lines = [
102
113
  "# Agent Narration Brief",
@@ -111,8 +122,7 @@ def build_agent_brief(
111
122
  f"- Target duration (cut mode): {target_duration}",
112
123
  f"- Effective speech budget: {effective_rate:.2f} Chinese chars/sec after {breath_sec:.2f}s pause allowance",
113
124
  ]
114
- # Understanding validates evidence before calling; the standalone script skill
115
- # has no producer dependency and must not invent an available ASR status.
125
+ # A caller that passes no ASR evidence must not get an invented available status.
116
126
  if asr_evidence is None:
117
127
  asr_evidence = {"status": "MISSING_OR_STALE", "glossary_modifications": None}
118
128
 
@@ -173,7 +183,7 @@ def build_agent_brief(
173
183
  "- `visual_audio_board.json` owns the exact picture/performance/reaction, entry/exit reason, original-audio anchor, `audio_owner`, and `narration_job` for each beat.",
174
184
  "- Only after the story/edit decisions are coherent, author `style_card.json` from `--style`, evidence, and user preference. It owns voice and pacing, not story structure, and is not a preset enum, fixed taxonomy, title plan, or packaging promise.",
175
185
  "- `packaging_plan.json` is optional and deferred until content lock unless the user explicitly asks for packaging. It must express the story's truthful promise, never drive or distort it.",
176
- "- `deslop_qc.json` is deterministic report-only tool QC: do not hand-author it, do not treat it as an AIGC detector, and do not auto-rewrite from it. Corrections remain human/agent rewrite work guided by objective blockers and advisory readability signals.",
186
+ "- `narration_lint.json.deslop_qc` is deterministic report-only tool QC written by validation: do not treat it as an AIGC detector, and do not auto-rewrite from it. Corrections remain human/agent rewrite work guided by objective blockers and advisory readability signals.",
177
187
  "",
178
188
  ]
179
189
  )
@@ -194,6 +204,7 @@ def build_agent_brief(
194
204
  )
195
205
  )
196
206
  lines.extend(_format_substrate_warning(substrate))
207
+ lines.extend(_format_moderation_refusals(scenes_analysis))
197
208
  lines.extend(_format_research_directive(work_dir, substrate))
198
209
  lines.extend(_format_background_research(_load_background_research(work_dir)))
199
210
  lines.extend(_format_consolidation(consolidation_index))
@@ -205,7 +216,8 @@ def build_agent_brief(
205
216
  asr_result,
206
217
  silence_periods,
207
218
  )
208
- if edit_mode == "cut" and (Path(work_dir) / "edited_source.mp4").exists():
219
+ cut_pass2 = edit_mode == "cut" and (Path(work_dir) / "edited_source.mp4").exists()
220
+ if cut_pass2:
209
221
  chunk_scenes, chunk_asr, _ = _remap_brief_evidence_to_output_timeline(
210
222
  work_dir, scenes_analysis, asr_for_chunks, [], required=True
211
223
  )
@@ -237,7 +249,7 @@ def build_agent_brief(
237
249
  cut_target_example = (
238
250
  target_duration if target_duration != "(not set)" else "30m"
239
251
  )
240
- if not (Path(work_dir) / "edited_source.mp4").exists():
252
+ if not cut_pass2:
241
253
  # PASS 1 of 2 (cut-first): pick the footage. Narration comes AFTER the cut is
242
254
  # rendered, so it can be written against the real OUTPUT timeline — no source->output
243
255
  # mapping, no silent drop/clamp, no desync.
@@ -259,7 +271,7 @@ def build_agent_brief(
259
271
  "- Clip length follows the moment. Vary pace; after any cold-open, order clips by causality so the cut reads as one coherent story, not a flat highlights reel.",
260
272
  "- Inspect dense scene-change candidates before locking boundaries. For source-authored cuts, delete irrelevant short shots and extend relevant shots to a complete action/reaction; for edit-created joins, move boundaries, restore same-source motion, or merge clips so the artificial cut disappears where possible. Do not hide a bad join with a transition.",
261
273
  "- Preserve complete spoken lines, but do not infer completeness from ASR window ends or quiet-window suggestions. Directly listen and inspect picture around each proposed boundary, then place the cut after the verified utterance/reaction; automatic snapping is only an advisory candidate.",
262
- "- Advisory does not mean absent: the cut CLI still snaps clip ends to sentence boundaries by default and blocks a cut that lands mid-sentence, using `silence_periods.json` and `speech_boundary_anchors.json` as the executable safety net under your listening.",
274
+ "- Advisory does not mean absent: the cut CLI still snaps clip ends to sentence boundaries by default and blocks a cut inside ASR-detected speech unless it lands in a quiet window or a sentence-end pause estimate, using `silence_periods.json` and `speech_boundary_anchors.json` as the executable safety net under your listening.",
263
275
  "",
264
276
  "### clip_plan.json shape (original source timestamps)",
265
277
  "",
@@ -366,22 +378,45 @@ def build_agent_brief(
366
378
  '- ✗ "一个蒙眼的男人抱着一个篮子走在雨里。" (just describes the frame)',
367
379
  '- ✓ "护送者本可以独自离开,却为了保护那个孩子,主动把追兵引向自己。" (who, why, stakes)',
368
380
  "",
369
- "## Scene timing guide",
370
- "",
371
381
  ]
372
382
  )
383
+ # Cut pass 2 narrates edited_source.mp4, so its guide lists only the kept footage on the
384
+ # OUTPUT clock; the source-time guide (cut-away scenes, credits) belongs to pass 1.
385
+ if cut_pass2:
386
+ guide_scenes, guide_asr, guide_silence = fusion_scenes, fusion_asr, fusion_silence
387
+ lines.extend(
388
+ [
389
+ "## Scene timing guide (OUTPUT time)",
390
+ "",
391
+ "Only footage kept in `edited_source.mp4`, timed on its OUTPUT timeline (0 .. total); "
392
+ "scenes the cut left out are not listed.",
393
+ "",
394
+ ]
395
+ )
396
+ else:
397
+ guide_scenes, guide_asr, guide_silence = scenes_analysis, asr_result, silence_periods
398
+ lines.extend(["## Scene timing guide", ""])
373
399
 
374
- for scene in scenes_analysis:
400
+ for scene in guide_scenes:
375
401
  duration = scene["end"] - scene["start"]
376
402
  max_chars = max(5, int(max(1.0, duration - breath_sec) * effective_rate))
377
- quiets = _quiet_windows_for_scene(silence_periods, scene)
403
+ quiets = _quiet_windows_for_scene(guide_silence, scene)
378
404
  quiet_text = ", ".join(f"{s:.1f}-{e:.1f}s" for s, e in quiets) or "none"
405
+ span = f"{scene['start']:.1f}-{scene['end']:.1f}s"
379
406
  lines.extend(
380
407
  [
381
- f"### Scene {scene['scene_id'] + 1}: {scene['start']:.1f}-{scene['end']:.1f}s",
408
+ f"### OUTPUT {span} (source scene {_scene_number(scene['scene_id'])})"
409
+ if cut_pass2
410
+ else f"### Scene {_scene_number(scene['scene_id'])}: {span}",
382
411
  f"- Duration: {duration:.1f}s; max budget if fully narrated: {max_chars} chars",
383
412
  f"- Quiet windows: {quiet_text}",
384
- f"- Description: {scene.get('description', '')}",
413
+ f"- Description: {scene.get('description', '')}"
414
+ + (
415
+ " [moderation_refused]"
416
+ if scene.get("analysis_status") == "moderation_refused"
417
+ and scene.get("description_source") != "mimo_video_overview"
418
+ else ""
419
+ ),
385
420
  ]
386
421
  )
387
422
  if scene.get("depth_analysis"):
@@ -389,7 +424,7 @@ def build_agent_brief(
389
424
  facts = _format_frame_facts(scene)
390
425
  if facts:
391
426
  lines.append(facts.rstrip())
392
- asr_lines = _scene_asr_lines(asr_result, scene)
427
+ asr_lines = _scene_asr_lines(guide_asr, scene)
393
428
  if asr_lines:
394
429
  lines.append("- ASR overlap:")
395
430
  lines.extend(asr_lines[:8])
@@ -397,6 +432,5 @@ def build_agent_brief(
397
432
 
398
433
  brief_path = Path(work_dir) / "agent_narration_brief.md"
399
434
  brief_path.write_text("\n".join(lines), encoding="utf-8")
400
- _write_deslop_qc_requirements(work_dir)
401
435
  log(f"已写入 Agent 解说写作 brief: {brief_path}")
402
436
  return brief_path
@@ -4,9 +4,17 @@ import json
4
4
  import re
5
5
  from pathlib import Path
6
6
 
7
+ from index_normalize import normalize_index
7
8
  from lib import CONFIG, file_identity
8
9
 
9
10
 
11
+ def _scene_number(scene_id):
12
+ """1-based scene number for every human-facing label in the brief (JSON keeps the
13
+ 0-based `scene_id`); a scene split across kept clips ("3.1") reads "4 part 2"."""
14
+ base, _, part = str(scene_id).partition(".")
15
+ return f"{int(base) + 1} part {int(part) + 1}" if part else str(int(base) + 1)
16
+
17
+
10
18
  def _load_background_research(work_dir):
11
19
  """Load the agent-authored background_research.json ({} when absent)."""
12
20
  path = Path(work_dir) / "background_research.json"
@@ -158,6 +166,31 @@ def _format_substrate_warning(assessment):
158
166
  ]
159
167
 
160
168
 
169
+ def _format_moderation_refusals(scenes_analysis):
170
+ """One brief line naming the scenes MiMo moderation refused, so blank descriptions read as
171
+ refusals (vlm.analyze_scenes sets analysis_status), not as empty footage."""
172
+ refused = [
173
+ scene for scene in scenes_analysis or []
174
+ if isinstance(scene, dict) and scene.get("analysis_status") == "moderation_refused"
175
+ ]
176
+ if not refused:
177
+ return []
178
+ labels = ", ".join(_scene_number(scene["scene_id"]) for scene in refused[:12])
179
+ more = f" …(+{len(refused) - 12})" if len(refused) > 12 else ""
180
+ covered = sum(
181
+ 1 for scene in refused if scene.get("description_source") == "mimo_video_overview"
182
+ )
183
+ overview_note = f"; {covered} of them are described by the MiMo overview instead" if covered else ""
184
+ return [
185
+ f"- Moderation-refused scenes: {len(refused)}/{len(scenes_analysis)} "
186
+ f"(Scene {labels}{more}) — MiMo refused the frame VLM request, so their frame "
187
+ f"description is blank and they carry no frame_facts{overview_note}. This is a "
188
+ "content-moderation refusal, not empty footage: ground them in ASR, burned-in "
189
+ "subtitles, or background research, or skip them in cut mode.",
190
+ "",
191
+ ]
192
+
193
+
161
194
  def _write_json_artifact(work_dir, name, payload):
162
195
  path = Path(work_dir) / name
163
196
  path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@@ -175,7 +208,7 @@ def _format_asr_chunks_for_brief(chunks, max_chunks=24):
175
208
  "",
176
209
  ]
177
210
  for chunk in chunks[:max_chunks]:
178
- scene_ids = ",".join(str(sid) for sid in chunk["scene_ids"]) or "n/a"
211
+ scene_ids = ", ".join(_scene_number(sid) for sid in chunk["scene_ids"]) or "n/a"
179
212
  text = chunk["text"]
180
213
  if len(text) > 900:
181
214
  text = text[:897] + "..."
@@ -223,7 +256,7 @@ def _format_timeline_fusion_for_brief(fusion, max_items=40):
223
256
  )
224
257
  lines.extend(
225
258
  [
226
- f"### Fusion scene {item['scene_id']}: {start:.1f}-{end:.1f}s ({item['recommended_mode']})",
259
+ f"### Fusion scene {_scene_number(item['scene_id'])}: {start:.1f}-{end:.1f}s ({item['recommended_mode']})",
227
260
  f"- Visual: {item['visual_description']}",
228
261
  f"- Dialogue overlap: {item['dialogue_overlap_seconds']:.1f}s | {dialogue_text}",
229
262
  f"- Narration slots: {slot_text}",
@@ -278,7 +311,10 @@ def _load_consolidation(work_dir, scenes_analysis):
278
311
  return {}
279
312
  if not all(isinstance(item, dict) for item in index["relationships"]):
280
313
  return {}
281
- return index
314
+ # An index written before the deterministic repairs existed is repaired here too, so a
315
+ # --brief-only run does not wait for the next full understand.py run (pure, no model call).
316
+ # No duration cap: the brief's scenes may be an edited timeline, plot times are source times.
317
+ return normalize_index(index)[0]
282
318
 
283
319
 
284
320
  def _format_consolidation(index):
@@ -3,24 +3,18 @@
3
3
  import json
4
4
  from pathlib import Path
5
5
 
6
- from lib import CONFIG, file_identity
6
+ from lib import (
7
+ CONFIG,
8
+ file_identity,
9
+ is_usable_overview_chunk,
10
+ offline_ignored_settings,
11
+ settings_match,
12
+ )
7
13
  from briefing.context import _consolidation_model
8
14
 
9
- # Shared with consolidate.py, which this byte-identical copy cannot import (the sibling
10
- # skill ships no consolidate.py). Keep both literals in sync.
15
+ # Same literal as consolidate._ASR_SPAN_TOL; test_asr_span_tol_matches_across_files pins them.
11
16
  _ASR_SPAN_TOL = 0.05
12
17
 
13
- _MIMO_REJECTION_MARKERS = (
14
- "request was rejected",
15
- "considered high risk",
16
- "high risk",
17
- "content policy",
18
- "cannot process",
19
- "无法处理",
20
- "内容审核",
21
- "违规",
22
- )
23
-
24
18
 
25
19
  def _load_clean_asr(work_dir, asr_result):
26
20
  """Return consolidate.py's cleaned ASR segments, or None to fall back to raw asr_result.
@@ -70,15 +64,6 @@ def _load_clean_asr(work_dir, asr_result):
70
64
  return segments
71
65
 
72
66
 
73
- def _is_mimo_chunk_usable(content):
74
- """A chunk is usable only if MiMo returned real analysis (not empty / a moderation refusal)."""
75
- text = (content or "").strip()
76
- if not text:
77
- return False
78
- low = text.lower()
79
- return not any(marker in low for marker in _MIMO_REJECTION_MARKERS)
80
-
81
-
82
67
  def _mimo_video_settings():
83
68
  """Non-secret MiMo video-overview settings that affect generated content."""
84
69
  return {
@@ -139,7 +124,7 @@ def _mimo_overview_matches_current_inputs(overview, scenes, video_path=None):
139
124
  if not isinstance(chunks, list) or not all(
140
125
  isinstance(chunk, dict)
141
126
  and isinstance(chunk.get("content"), str)
142
- and _is_mimo_chunk_usable(chunk["content"])
127
+ and is_usable_overview_chunk(chunk["content"])
143
128
  for chunk in chunks
144
129
  ):
145
130
  return False
@@ -153,7 +138,8 @@ def _mimo_overview_matches_current_inputs(overview, scenes, video_path=None):
153
138
  cached_keys = [_mimo_chunk_cache_key(chunk) for chunk in chunks]
154
139
  except (KeyError, TypeError, ValueError):
155
140
  return False
156
- return overview.get("settings") == settings and cached_keys == expected_keys
141
+ ignored = offline_ignored_settings(CONFIG["mimo_video_api_key"], "mimo_video_api_url")
142
+ return settings_match(overview.get("settings"), settings, ignored) and cached_keys == expected_keys
157
143
 
158
144
 
159
145
  def _load_mimo_overview_for_brief(work_dir, scenes, enabled=None, video_path=None):
@@ -242,10 +228,12 @@ def _format_optional_stage_warnings(
242
228
  work_dir, "consolidation.status.json"
243
229
  )
244
230
  if consolidation_status is not None and consolidation_status["enabled"]:
245
- if consolidation_status["status"] == "failed":
231
+ if consolidation_status["status"] in {"failed", "skipped_no_key"}:
246
232
  warnings.append(
247
233
  _optional_stage_warning(
248
- "consolidation", "failed", consolidation_status["message"]
234
+ "consolidation",
235
+ consolidation_status["status"],
236
+ consolidation_status["message"],
249
237
  )
250
238
  )
251
239
  elif consolidation_status.get("do_index") is True and not consolidation_index: