@oh-story/dsh 0.1.7 → 0.1.9

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 (272) hide show
  1. package/README.md +30 -15
  2. package/lib/client.js +93 -16
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +165 -69
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
  8. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
  11. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  12. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  21. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  22. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
  23. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
  24. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
  25. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
  26. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
  27. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
  29. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  30. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  31. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  32. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  33. package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
  34. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
  35. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
  36. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  37. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  38. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  39. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
  40. package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
  41. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
  42. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
  43. package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
  44. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  45. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
  46. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  47. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
  50. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
  51. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
  53. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
  54. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
  55. package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
  56. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  57. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  58. package/lib/index.js +4 -2
  59. package/lib/index.js.map +2 -2
  60. package/lib/novel-to-game/VERSION +1 -1
  61. package/lib/novel-to-game/manifest.json +28 -28
  62. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
  64. package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
  65. package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
  66. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
  67. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
  68. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
  69. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
  70. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
  71. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
  72. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
  73. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
  74. package/lib/oh-story/manifest.json +292 -172
  75. package/lib/oh-story/roles/character-designer.md +1 -1
  76. package/lib/oh-story/roles/narrative-writer.md +56 -242
  77. package/lib/oh-story/roles/story-architect.md +1 -1
  78. package/lib/oh-story/skills/story/SKILL.md +1 -1
  79. package/lib/oh-story/skills/story/VERSION +1 -1
  80. package/lib/oh-story/skills/story/references/author-memory.md +10 -8
  81. package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
  82. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
  83. package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
  84. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  85. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  86. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  87. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  88. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  89. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  90. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  91. package/lib/oh-story/skills/story-import/SKILL.md +2 -2
  92. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  93. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  94. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
  95. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
  96. package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
  97. package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
  98. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
  99. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
  100. package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
  101. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  102. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  103. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
  104. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
  105. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  106. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  107. package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
  108. package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
  109. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  110. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  111. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  112. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  113. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  114. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
  115. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
  116. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
  117. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
  118. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  119. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
  120. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
  121. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  122. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  123. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
  124. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  125. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  126. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
  127. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  128. package/lib/oh-story/skills/story-review/SKILL.md +13 -3
  129. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
  130. package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
  131. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  132. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
  133. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
  134. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  135. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  136. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
  137. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  138. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  139. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  140. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  141. package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
  142. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
  143. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  144. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  145. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
  146. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  147. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  148. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  149. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  150. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  151. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
  152. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  153. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  154. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  155. package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
  156. package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
  157. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  158. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  159. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
  160. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
  161. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  162. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  163. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
  164. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  165. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  166. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
  167. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  168. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  169. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  170. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  171. package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
  172. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  173. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  174. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  175. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  176. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  177. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
  178. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  179. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  180. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  181. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  182. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  183. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  184. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  185. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  186. package/lib/types/client/index.d.ts.map +1 -1
  187. package/lib/types/client/production-prompts.d.ts.map +1 -1
  188. package/lib/types/client/production-runtime.d.ts +17 -0
  189. package/lib/types/client/production-runtime.d.ts.map +1 -1
  190. package/lib/types/skill-provider.d.ts.map +1 -1
  191. package/lib/video-recap/CHANGELOG.md +31 -1
  192. package/lib/video-recap/manifest.json +162 -162
  193. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  194. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  195. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  196. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  197. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  198. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  199. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  200. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  201. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  202. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  203. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  204. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  205. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  206. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  207. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  208. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  209. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  210. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  211. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  212. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  213. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  214. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  215. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  216. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  217. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  218. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  219. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  220. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  221. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  222. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  223. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  224. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  225. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  226. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  227. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  228. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  229. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  230. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  231. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  232. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  233. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  234. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  235. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  236. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  237. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  238. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  239. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  240. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  241. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  242. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  243. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  244. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  245. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  246. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  247. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  248. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  249. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  250. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  251. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  252. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  253. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  254. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  255. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  256. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  257. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  258. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  259. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  260. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  262. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  263. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  264. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  265. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  266. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  267. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  268. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  269. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  270. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  271. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  272. package/package.json +41 -41
@@ -11,7 +11,6 @@ from recap_runtime import (
11
11
  _build_multi_source_records,
12
12
  _coerce_videos,
13
13
  _entry,
14
- _file_fingerprint,
15
14
  _material_settings_fingerprint,
16
15
  _optional_env_int,
17
16
  _preflight_burn_subtitles,
@@ -59,7 +58,7 @@ RUN_MANIFEST = "recap_run_manifest.json"
59
58
  def _voiceover_args(work_dir, narration_path, args):
60
59
  """Build the shared full/cut invocation for video-voiceover."""
61
60
  result = ["--work-dir", str(work_dir), "--narration", str(narration_path)]
62
- if getattr(args, "tts_provider", "auto") != "auto":
61
+ if args.tts_provider != "auto":
63
62
  result += ["--tts-provider", args.tts_provider]
64
63
  if args.mimo_tts_voice:
65
64
  result += ["--mimo-voice", args.mimo_tts_voice]
@@ -75,25 +74,23 @@ def _run_or_restore_understanding(source_record, source_work_dir, args):
75
74
  source_work_dir = Path(source_work_dir)
76
75
  source_work_dir.mkdir(parents=True, exist_ok=True)
77
76
  source_fp = source_record["source_video_fingerprint"]
78
- settings_fp = source_record.get(
79
- "settings_fingerprint"
80
- ) or _material_settings_fingerprint(args)
77
+ settings_fp = source_record["settings_fingerprint"]
81
78
  lib_dir = _material_library_dir(args)
82
79
  restored = False
83
- if lib_dir and _materials_enabled(args):
80
+ if _materials_enabled(args):
84
81
  result = material_lib.restore_material(
85
82
  lib_dir,
86
83
  source_work_dir,
87
84
  source_fingerprint=source_fp,
88
85
  settings_fp=settings_fp,
89
86
  )
90
- restored = bool(result.get("restored"))
87
+ restored = result["restored"]
91
88
  if restored:
92
89
  print(
93
- f"[video-recap] ♻️ 复用素材库: {result.get('material_id')} → {source_work_dir}",
90
+ f"[video-recap] ♻️ 复用素材库: {result['material_id']} → {source_work_dir}",
94
91
  flush=True,
95
92
  )
96
- elif result.get("reason"):
93
+ else:
97
94
  print(
98
95
  f"[video-recap] 素材库未复用 {source_record['source_name']}: {result['reason']}",
99
96
  flush=True,
@@ -107,19 +104,19 @@ def _run_or_restore_understanding(source_record, source_work_dir, args):
107
104
  )
108
105
 
109
106
  _write_run_manifest(source_work_dir, source_record["source_path"], args)
110
- if lib_dir and _save_materials_enabled(args):
107
+ if _save_materials_enabled(args):
111
108
  meta = material_lib.save_material(
112
109
  lib_dir,
113
110
  source_work_dir,
114
111
  source_record["source_path"],
115
112
  source_fp,
116
113
  settings_fp,
117
- source_id=source_record.get("source_id"),
118
- material_id=source_record.get("material_id"),
114
+ source_id=source_record["source_id"],
115
+ material_id=source_record["material_id"],
119
116
  )
120
- source_record["material_id"] = meta.get("material_id")
117
+ source_record["material_id"] = meta["material_id"]
121
118
  print(
122
- f"[video-recap] 💾 已沉淀素材: {meta.get('material_id')} → {lib_dir}",
119
+ f"[video-recap] 💾 已沉淀素材: {meta['material_id']} → {lib_dir}",
123
120
  flush=True,
124
121
  )
125
122
  return restored
@@ -127,7 +124,7 @@ def _run_or_restore_understanding(source_record, source_work_dir, args):
127
124
 
128
125
  def _single_source_record(video, args):
129
126
  """Identity + settings for the one source of a single-video run."""
130
- fp = _file_fingerprint(video)
127
+ fp = material_lib.file_fingerprint(video)
131
128
  return {
132
129
  "source_id": material_lib.source_id_from_fingerprint(fp),
133
130
  "source_path": str(video),
@@ -209,9 +206,9 @@ def _run_multi_cut(videos, work_dir, args):
209
206
  ]
210
207
  if args.target_duration:
211
208
  crender += ["--target-duration", args.target_duration]
212
- if getattr(args, "allow_duration_drift", False):
209
+ if args.allow_duration_drift:
213
210
  crender.append("--allow-duration-drift")
214
- if getattr(args, "allow_sparse_cut", False):
211
+ if args.allow_sparse_cut:
215
212
  crender.append("--allow-sparse-cut")
216
213
  _run("video-cut", "cut.py", *crender)
217
214
  cut_qc = _surface_cut_qc(work_dir)
@@ -301,10 +298,7 @@ def _run_multi_cut(videos, work_dir, args):
301
298
  aargs.append("--jianying-no-bundle-media")
302
299
  _run("video-assemble", "assemble.py", *aargs)
303
300
 
304
- final_dir = Path(args.output_dir) if args.output_dir else work_dir.parent
305
- final_output = _read_assembly_output(work_dir) or (
306
- final_dir / ("recap_" + recap_stem + ".mp4")
307
- )
301
+ final_output = _read_assembly_output(work_dir)
308
302
  _write_shift_left_stage_qc(
309
303
  work_dir,
310
304
  "post_render",
@@ -325,7 +319,7 @@ def main():
325
319
  ap.error(
326
320
  "MIMO_QC/--mimo-qc must be one of: off, pre-assemble, post-render, both"
327
321
  )
328
- if getattr(args, "tts_provider", "auto") not in TTS_PROVIDERS:
322
+ if args.tts_provider not in TTS_PROVIDERS:
329
323
  ap.error(
330
324
  "TTS_PROVIDER/--tts-provider must be one of: auto, mimo-tts, fish-audio"
331
325
  )
@@ -354,9 +348,7 @@ def main():
354
348
  )
355
349
  if explicit_mimo_voice and args.voice_ref:
356
350
  ap.error("--mimo-tts-voice and --voice-ref are mutually exclusive")
357
- if getattr(args, "tts_provider", "auto") == "fish-audio" and (
358
- explicit_mimo_voice or args.voice_ref
359
- ):
351
+ if args.tts_provider == "fish-audio" and (explicit_mimo_voice or args.voice_ref):
360
352
  ap.error(
361
353
  "--mimo-tts-voice/--voice-ref are only supported by --tts-provider mimo-tts"
362
354
  )
@@ -532,9 +524,9 @@ def main():
532
524
  crender = [str(video), "--work-dir", str(work_dir), "--no-narration-map"]
533
525
  if args.target_duration:
534
526
  crender += ["--target-duration", args.target_duration]
535
- if getattr(args, "allow_duration_drift", False):
527
+ if args.allow_duration_drift:
536
528
  crender.append("--allow-duration-drift")
537
- if getattr(args, "allow_sparse_cut", False):
529
+ if args.allow_sparse_cut:
538
530
  crender.append("--allow-sparse-cut")
539
531
  _run("video-cut", "cut.py", *crender)
540
532
  cut_qc = _surface_cut_qc(work_dir)
@@ -638,10 +630,7 @@ def main():
638
630
  aargs.append("--jianying-no-bundle-media")
639
631
  _run("video-assemble", "assemble.py", *aargs)
640
632
 
641
- final_dir = Path(args.output_dir) if args.output_dir else work_dir.parent
642
- final_output = _read_assembly_output(work_dir) or (
643
- final_dir / ("recap_" + video.stem + ".mp4")
644
- )
633
+ final_output = _read_assembly_output(work_dir)
645
634
  _write_shift_left_stage_qc(
646
635
  work_dir,
647
636
  "post_render",
@@ -1,25 +1,17 @@
1
- """Provide subprocess, manifest, review, and preflight runtime helpers."""
2
-
3
- import hashlib
1
+ """Provide subprocess, manifest, and preflight runtime helpers."""
4
2
 
5
3
  import json
6
-
7
4
  import math
8
-
9
5
  import os
10
-
11
6
  import shlex
12
-
7
+ import shutil
13
8
  import subprocess
14
-
15
9
  import sys
16
-
17
10
  from pathlib import Path
18
11
 
19
-
20
- from doctor import ffmpeg_has_subtitles_filter
21
-
22
12
  import materials as material_lib
13
+ from doctor import ffmpeg_has_subtitles_filter
14
+ from lib import env_bool, load_json
23
15
 
24
16
  BUNDLE = Path(__file__).resolve().parents[2] # the skills/ directory
25
17
 
@@ -36,13 +28,6 @@ def _run(skill, script, *cli_args):
36
28
  raise SystemExit(f"{skill}/{script} 失败 (exit {res.returncode})")
37
29
 
38
30
 
39
- def _env_bool(name, default=False):
40
- raw = os.environ.get(name)
41
- if raw is None or raw == "":
42
- return default
43
- return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
44
-
45
-
46
31
  def _optional_env_int(name):
47
32
  raw = os.environ.get(name)
48
33
  if raw is None or raw.strip() == "":
@@ -73,8 +58,8 @@ def _read_video_duration_or_raise(path):
73
58
  raise SystemExit(f"无法读取成片时长: {path} ({detail})")
74
59
  try:
75
60
  duration = float(res.stdout.strip())
76
- except (TypeError, ValueError):
77
- raise SystemExit(f"无法读取成片时长: {path} (ffprobe 输出无效: {res.stdout!r})")
61
+ except ValueError:
62
+ raise SystemExit(f"无法读取成片时长: {path} (ffprobe 输出无效: {res.stdout!r})") from None
78
63
  if not math.isfinite(duration) or duration <= 0:
79
64
  raise SystemExit(f"无法读取成片时长: {path} (duration={duration:.3f})")
80
65
  return duration
@@ -96,79 +81,35 @@ def _probe_display_height_or_raise(path, *, require_square_pixels=False):
96
81
  ]
97
82
  res = subprocess.run(cmd, capture_output=True, text=True)
98
83
  try:
99
- stream = (json.loads(res.stdout or "{}").get("streams") or [])[0]
84
+ stream = json.loads(res.stdout)["streams"][0]
100
85
  width, height = int(stream["width"]), int(stream["height"])
101
- except (IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError):
86
+ except (IndexError, KeyError, ValueError):
102
87
  detail = (res.stderr or res.stdout or "ffprobe failed").strip()
103
- raise SystemExit(f"无法读取视频画布: {path} ({detail})")
104
- sar = str(stream.get("sample_aspect_ratio") or "1:1")
88
+ raise SystemExit(f"无法读取视频画布: {path} ({detail})") from None
89
+ sar = stream.get("sample_aspect_ratio", "1:1")
105
90
  try:
106
91
  num, den = sar.split(":", 1)
107
92
  sar_ratio = float(num) / float(den)
108
- display_width = max(1, int(round(width * sar_ratio)))
109
- except (TypeError, ValueError, ZeroDivisionError):
93
+ except (ValueError, ZeroDivisionError):
110
94
  sar_ratio = math.nan
111
- display_width = width
112
95
  if require_square_pixels and (
113
96
  not math.isfinite(sar_ratio) or abs(sar_ratio - 1.0) >= 1e-9
114
97
  ):
115
98
  raise SystemExit(
116
99
  f"subtitle Y coordinates currently require square-pixel video (SAR 1:1); got {sar}"
117
100
  )
101
+ display_width = max(1, round(width * sar_ratio)) if math.isfinite(sar_ratio) else width
118
102
  rotation_values = [
119
- (stream.get("tags") or {}).get("rotate"),
120
- *(
121
- item.get("rotation")
122
- for item in stream.get("side_data_list") or []
123
- if isinstance(item, dict)
124
- ),
103
+ stream.get("tags", {}).get("rotate"),
104
+ *(item.get("rotation") for item in stream.get("side_data_list", [])),
125
105
  ]
126
- rotation = 0
127
- for value in rotation_values:
128
- if value not in (None, ""):
129
- try:
130
- rotation = int(round(float(value))) % 360
131
- break
132
- except (TypeError, ValueError):
133
- pass
106
+ rotation = next(
107
+ (int(round(float(value))) % 360 for value in rotation_values if value not in (None, "")),
108
+ 0,
109
+ )
134
110
  return display_width if rotation in {90, 270} else height
135
111
 
136
112
 
137
- _FILE_FINGERPRINT_MEMO = {}
138
-
139
-
140
- def _file_identity(path):
141
- """(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
142
- st = os.stat(os.fspath(path))
143
- return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
144
-
145
-
146
- def _file_fingerprint(path, chunk_size=1024 * 1024):
147
- """Return a full-content fingerprint for cache-correct identity checks.
148
-
149
- The digest covers CONTENT only — never the path or mtime — so a copied video or
150
- artifact is still recognised as the same asset, while any byte change invalidates
151
- the cache even if timestamps, size, head, or tail bytes are misleading.
152
-
153
- Identity metadata is used ONLY to memoize within a single process. One understanding
154
- run fingerprints the same source video 8-10 times and the whole extracted frame set
155
- 2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
156
- any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
157
- re-hashed, so the memo can never serve a stale digest.
158
- """
159
- key = _file_identity(path)
160
- memoized = _FILE_FINGERPRINT_MEMO.get(key)
161
- if memoized is not None:
162
- return memoized
163
- h = hashlib.sha256()
164
- with open(os.fspath(path), "rb") as f:
165
- for chunk in iter(lambda: f.read(chunk_size), b""):
166
- h.update(chunk)
167
- digest = h.hexdigest()
168
- _FILE_FINGERPRINT_MEMO[key] = digest
169
- return digest
170
-
171
-
172
113
  def _analysis_settings(args):
173
114
  return {
174
115
  "context": args.context,
@@ -197,7 +138,7 @@ def _run_manifest_payload(video, args):
197
138
  return {
198
139
  "schema_version": 1,
199
140
  "source_video": str(Path(video).resolve()),
200
- "source_video_fingerprint": _file_fingerprint(video),
141
+ "source_video_fingerprint": material_lib.file_fingerprint(video),
201
142
  "settings": _analysis_settings(args),
202
143
  }
203
144
 
@@ -212,7 +153,7 @@ def _write_run_manifest(work_dir, video, args):
212
153
  def _build_multi_source_records(videos, args):
213
154
  records = []
214
155
  for video in _coerce_videos(videos):
215
- fp = _file_fingerprint(video)
156
+ fp = material_lib.file_fingerprint(video)
216
157
  records.append(
217
158
  {
218
159
  "source_path": str(video),
@@ -234,11 +175,11 @@ def _multi_run_manifest_payload(videos, args, source_records):
234
175
  "mode": "multi_source",
235
176
  "sources": [
236
177
  {
237
- "source_id": s.get("source_id"),
238
- "source_path": s.get("source_path"),
239
- "source_video_fingerprint": s.get("source_video_fingerprint"),
240
- "source_work_dir": s.get("source_work_dir"),
241
- "material_id": s.get("material_id"),
178
+ "source_id": s["source_id"],
179
+ "source_path": s["source_path"],
180
+ "source_video_fingerprint": s["source_video_fingerprint"],
181
+ "source_work_dir": s["source_work_dir"],
182
+ "material_id": s["material_id"],
242
183
  }
243
184
  for s in source_records
244
185
  ],
@@ -265,7 +206,7 @@ def _write_multi_source_manifest(work_dir, source_records):
265
206
  "source_name": s["source_name"],
266
207
  "source_video_fingerprint": s["source_video_fingerprint"],
267
208
  "source_work_dir": s["source_work_dir"],
268
- "material_id": s.get("material_id"),
209
+ "material_id": s["material_id"],
269
210
  }
270
211
  for s in source_records
271
212
  ],
@@ -275,32 +216,18 @@ def _write_multi_source_manifest(work_dir, source_records):
275
216
 
276
217
 
277
218
  def _load_run_manifest(work_dir):
219
+ """The run manifest, or None before Phase A has written one."""
278
220
  path = Path(work_dir) / RUN_MANIFEST
279
- if not path.exists():
280
- return None
281
- try:
282
- return json.loads(path.read_text(encoding="utf-8"))
283
- except (OSError, ValueError, TypeError):
284
- return None
285
-
286
-
287
- def _load_json(path):
288
- try:
289
- return json.loads(Path(path).read_text(encoding="utf-8"))
290
- except (OSError, ValueError, TypeError):
291
- return None
221
+ return load_json(path) if path.exists() else None
292
222
 
293
223
 
294
224
  def _burn_subtitles_intended(args):
295
225
  """Effective burn-subtitles state at orchestrator level. Mirrors video-assemble's
296
226
  CONFIG default `env_bool("BURN_SUBTITLES", True)` (burn is ON by default); an explicit
297
227
  CLI flag (--burn-subtitles / --no-burn-subtitles) overrides the env."""
298
- if getattr(args, "burn_subtitles", None) is not None:
299
- return bool(args.burn_subtitles)
300
- raw = os.environ.get("BURN_SUBTITLES")
301
- if raw is None or raw == "":
302
- return True
303
- return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
228
+ if args.burn_subtitles is not None:
229
+ return args.burn_subtitles
230
+ return env_bool("BURN_SUBTITLES", True)
304
231
 
305
232
 
306
233
  def _ffmpeg_present_but_cannot_burn():
@@ -309,8 +236,6 @@ def _ffmpeg_present_but_cannot_burn():
309
236
  entirely: that is a more fundamental problem that surfaces at the first stage (understand
310
237
  calls ffprobe/ffmpeg) and is reported by doctor, so this guard stays narrow — and does
311
238
  not fire in mocked, ffmpeg-less test environments."""
312
- import shutil
313
-
314
239
  if shutil.which("ffmpeg") is None:
315
240
  return False
316
241
  return not ffmpeg_has_subtitles_filter()
@@ -1,126 +1,71 @@
1
1
  """Write shift-left, MiMo, and final QC stage reports."""
2
2
 
3
3
  import json
4
-
5
-
6
4
  from pathlib import Path
7
5
 
8
- import qc_contract
9
-
10
6
  import final_qc
11
-
12
7
  import mimo_qc
13
-
14
-
15
- from recap_runtime import _load_json
8
+ import qc_contract
16
9
 
17
10
  ASSEMBLY_MANIFEST = "assembly_manifest.json"
18
-
19
-
20
- def _json_safe(value):
21
- """Return a JSON-serializable, secret-redacted copy for local QC metadata."""
22
-
23
- def convert(item):
24
- if isinstance(item, Path):
25
- return str(item)
26
- if isinstance(item, dict):
27
- return {str(k): convert(v) for k, v in item.items()}
28
- if isinstance(item, (list, tuple)):
29
- return [convert(v) for v in item]
30
- return item
31
-
32
- return qc_contract.redact_secrets(convert(value))
11
+ PREFLIGHT_QC = "preflight_qc.json"
33
12
 
34
13
 
35
14
  def _load_preflight_stage_reports(work_dir):
36
- path = Path(work_dir) / "preflight_qc.json"
15
+ path = Path(work_dir) / PREFLIGHT_QC
37
16
  if not path.exists():
38
17
  return {}
39
- data = _load_json(path)
40
- if not isinstance(data, dict):
41
- raise qc_contract.QCContractError("preflight_qc.json must be a JSON object")
42
- qc_contract.validate_report(data)
43
- stages = None
44
- metadata = data.get("metadata")
45
- if isinstance(metadata, dict) and isinstance(metadata.get("stages"), dict):
46
- stages = metadata.get("stages")
47
- elif isinstance(data.get("stages"), dict):
48
- stages = data.get("stages")
49
- if stages is None:
50
- return {data["stage"]: data}
51
- stage_reports = {}
52
- for stage, report in stages.items():
53
- if not isinstance(report, dict):
54
- raise qc_contract.QCContractError(
55
- f"preflight stage report must be an object: {stage}"
56
- )
57
- qc_contract.validate_report(report)
58
- if report.get("stage") != stage:
59
- raise qc_contract.QCContractError(
60
- f"preflight stage key does not match report stage: {stage}"
61
- )
62
- stage_reports[stage] = report
63
- return stage_reports
64
-
65
-
66
- def _write_shift_left_stage_qc(work_dir, stage, metadata=None, findings=None):
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):
67
22
  """Write/roll up local shift-left QC for one pipeline stage.
68
23
 
69
- This is a local contract artifact only: no MiMo/deep eval calls, no repair, and
70
- no credential persistence. Any validation/write failure is allowed to raise.
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).
71
26
  """
72
- work_dir = Path(work_dir)
73
- path = work_dir / "preflight_qc.json"
74
27
  stage_report = qc_contract.build_report(
75
- artifact="preflight_qc.json",
76
- stage=stage,
77
- findings=findings or [],
78
- metadata=_json_safe(metadata or {}),
28
+ artifact=PREFLIGHT_QC, stage=stage, findings=findings, metadata=metadata
79
29
  )
80
30
  stage_reports = _load_preflight_stage_reports(work_dir)
81
31
  stage_reports[stage] = stage_report
82
- aggregate_findings = []
83
- for report in stage_reports.values():
84
- aggregate_findings.extend(dict(f) for f in report.get("findings", []))
85
- top_metadata = dict(stage_report.get("metadata") or {})
32
+ top_metadata = dict(stage_report["metadata"])
86
33
  top_metadata["latest_stage"] = stage
87
34
  top_metadata["stages"] = stage_reports
88
35
  top_report = qc_contract.build_report(
89
- artifact="preflight_qc.json",
36
+ artifact=PREFLIGHT_QC,
90
37
  stage=stage,
91
- findings=aggregate_findings,
92
- metadata=_json_safe(top_metadata),
38
+ findings=[f for report in stage_reports.values() for f in report["findings"]],
39
+ metadata=top_metadata,
93
40
  )
94
- qc_contract.validate_report(top_report)
95
- path.write_text(
41
+ (Path(work_dir) / PREFLIGHT_QC).write_text(
96
42
  json.dumps(top_report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
97
43
  )
98
- qc_contract.validate_report(_load_json(path))
99
44
  return top_report
100
45
 
101
46
 
102
47
  def _tts_qc_metadata(work_dir):
103
48
  work_dir = Path(work_dir)
104
49
  metadata = {}
105
- tts_meta = _load_json(work_dir / "tts_meta.json")
106
- if tts_meta is not None:
107
- metadata["tts_meta"] = tts_meta
50
+ tts_meta = work_dir / "tts_meta.json"
51
+ if tts_meta.exists():
52
+ metadata["tts_meta"] = json.loads(tts_meta.read_text(encoding="utf-8"))
108
53
  tts_dir = work_dir / "tts_segments"
109
- if tts_dir.exists() and tts_dir.is_dir():
54
+ if tts_dir.is_dir():
110
55
  metadata["tts_segments"] = [
111
- p.relative_to(work_dir).as_posix()
112
- for p in sorted(tts_dir.iterdir())
113
- if p.is_file()
56
+ p.relative_to(work_dir).as_posix() for p in sorted(tts_dir.iterdir()) if p.is_file()
114
57
  ]
115
58
  return metadata
116
59
 
117
60
 
118
61
  def _post_render_qc_metadata(work_dir, final_output):
119
62
  work_dir = Path(work_dir)
120
- metadata = {"final_output": str(final_output) if final_output is not None else None}
121
- assembly_manifest = _load_json(work_dir / ASSEMBLY_MANIFEST)
122
- if assembly_manifest is not None:
123
- metadata["assembly_manifest"] = assembly_manifest
63
+ metadata = {"final_output": str(final_output)}
64
+ manifest = work_dir / ASSEMBLY_MANIFEST
65
+ if manifest.exists():
66
+ metadata["assembly_manifest"] = json.loads(
67
+ manifest.read_text(encoding="utf-8")
68
+ )
124
69
  return metadata
125
70
 
126
71
 
@@ -136,13 +81,11 @@ def _write_final_qc_reports(work_dir, final_output):
136
81
  def _print_final_qc_pointer(result):
137
82
  """Surface a report-only final_qc/golden_eval FAIL so the shift-left QC is
138
83
  not a silent no-op. Advisory only: it never changes the exit status."""
139
- if not isinstance(result, dict):
140
- return
141
- problems = []
142
- for key in ("final_qc", "golden_eval"):
143
- section = result.get(key)
144
- if isinstance(section, dict) and section.get("ok") is False:
145
- problems.append(f"{key} blocker_count={section.get('blocker_count', '?')}")
84
+ problems = [
85
+ f"{key} blocker_count={result[key].get('blocker_count', '?')}"
86
+ for key in ("final_qc", "golden_eval")
87
+ if result[key].get("ok") is False
88
+ ]
146
89
  if problems:
147
90
  print(
148
91
  "[video-recap] ⚠️ 最终 QC 未通过(仅报告,不阻断): "
@@ -152,47 +95,34 @@ def _print_final_qc_pointer(result):
152
95
 
153
96
 
154
97
  def _mimo_qc_stage_enabled(args, stage):
155
- mode = getattr(args, "mimo_qc", "off") or "off"
156
98
  return (
157
- mode == "both"
158
- or (mode == "pre-assemble" and stage == "pre_assemble")
159
- or (mode == "post-render" and stage == "post_render")
99
+ args.mimo_qc == "both"
100
+ or (args.mimo_qc == "pre-assemble" and stage == "pre_assemble")
101
+ or (args.mimo_qc == "post-render" and stage == "post_render")
160
102
  )
161
103
 
162
104
 
163
105
  def _prepare_mimo_qc(work_dir, args):
164
106
  """Remove an old advisory artifact when this run has MiMo QC disabled."""
165
- if getattr(args, "mimo_qc", "off") == "off":
107
+ if args.mimo_qc == "off":
166
108
  mimo_qc.clear_report(work_dir)
167
109
 
168
110
 
169
111
  def _print_mimo_qc_pointer(result, stage):
170
- report = result.get("report", {}) if isinstance(result, dict) else {}
171
- metadata = report.get("metadata", {}) if isinstance(report, dict) else {}
172
- status = metadata.get("status", "unknown")
173
- path = (
174
- result.get("path", "mimo_qc.json")
175
- if isinstance(result, dict)
176
- else "mimo_qc.json"
177
- )
178
- stage_findings = [
179
- finding
180
- for finding in report.get("findings", [])
181
- if isinstance(finding, dict) and finding.get("stage") == stage
182
- ]
112
+ report = result["report"]
113
+ metadata = report["metadata"]
114
+ status = metadata["status"]
115
+ stage_findings = [f for f in report["findings"] if f["stage"] == stage]
183
116
  if status in {"failed", "unavailable"}:
184
- reason = metadata.get("error") or "unavailable"
185
117
  print(
186
- f"[video-recap] ⚠ MiMo QC {stage}: {status} ({reason});建议性检查不可用,继续流水线"
118
+ f"[video-recap] ⚠ MiMo QC {stage}: {status} ({metadata['error']});建议性检查不可用,继续流水线"
187
119
  )
188
120
  return
189
121
  print(
190
- f"[video-recap] ℹ MiMo QC {stage}: {status}, {len(stage_findings)} 条建议;详见 {path}"
122
+ f"[video-recap] ℹ MiMo QC {stage}: {status}, {len(stage_findings)} 条建议;详见 {result['path']}"
191
123
  )
192
124
  for finding in stage_findings[:5]:
193
- print(
194
- f"[video-recap] - {finding.get('message') or finding.get('decision_reason')}"
195
- )
125
+ print(f"[video-recap] - {finding['message']}")
196
126
 
197
127
 
198
128
  def _run_mimo_qc_stage(work_dir, args, stage, *, final_output=None):
@@ -204,7 +134,7 @@ def _run_mimo_qc_stage(work_dir, args, stage, *, final_output=None):
204
134
  work_dir,
205
135
  stage=stage,
206
136
  live=True,
207
- refresh=bool(getattr(args, "mimo_qc_refresh", False)),
137
+ refresh=args.mimo_qc_refresh,
208
138
  final_output=final_output,
209
139
  )
210
140
  except Exception as exc: