@oh-story/dsh 0.1.8 → 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 +11 -11
  2. package/lib/client.js +35 -14
  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
@@ -4,6 +4,7 @@ import os
4
4
  import socket
5
5
  import urllib.error
6
6
  import urllib.request
7
+ from pathlib import Path
7
8
 
8
9
 
9
10
  # ── 配置 ──────────────────────────────────────────────────────────────
@@ -25,7 +26,7 @@ DEFAULT_FISH_TTS_REFERENCE_ID = "5653cea4ac83480aaf2bf45406556185"
25
26
 
26
27
  def normalize_api_url(raw_url):
27
28
  """Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
28
- url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
29
+ url = raw_url.rstrip("/")
29
30
  if url.endswith("/chat/completions"):
30
31
  return url
31
32
  return f"{url}/chat/completions"
@@ -33,34 +34,40 @@ def normalize_api_url(raw_url):
33
34
 
34
35
  def is_mimo_token_plan_key(api_key):
35
36
  """Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
36
- return str(api_key or "").strip().startswith("tp-")
37
+ return api_key.startswith("tp-")
37
38
 
38
39
 
39
- def default_mimo_api_url(api_key="", cluster=None):
40
+ def default_mimo_api_url(is_token_plan):
40
41
  """Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
41
42
 
42
43
  MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
43
44
  (`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
44
45
  not the pay-as-you-go `api.xiaomimimo.com` endpoint.
46
+
47
+ The caller classifies its own key with `is_mimo_token_plan_key` and passes only
48
+ that bit: a credential never reaches a function whose return value is logged.
45
49
  """
46
- if is_mimo_token_plan_key(api_key):
47
- cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
48
- cluster_name = str(cluster_name).strip().lower()
49
- return MIMO_TOKEN_PLAN_API_URLS.get(cluster_name, MIMO_TOKEN_PLAN_API_URLS[DEFAULT_MIMO_TOKEN_PLAN_CLUSTER])
50
- return DEFAULT_MIMO_API_URL
50
+ if not is_token_plan:
51
+ return DEFAULT_MIMO_API_URL
52
+ cluster = (os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER).strip().lower()
53
+ if cluster not in MIMO_TOKEN_PLAN_API_URLS:
54
+ raise ValueError(
55
+ f"MIMO_TOKEN_PLAN_CLUSTER must be one of {sorted(MIMO_TOKEN_PLAN_API_URLS)}; got {cluster!r}"
56
+ )
57
+ return MIMO_TOKEN_PLAN_API_URLS[cluster]
51
58
 
52
59
 
53
60
  def env_int(name, default, *, minimum=None):
54
- """Read an integer env var; ignore malformed values instead of crashing import."""
61
+ """Read an integer env var; a malformed or out-of-range value is a clear error."""
55
62
  raw = os.environ.get(name)
56
63
  if raw is None or raw == "":
57
64
  return default
58
65
  try:
59
66
  value = int(raw)
60
- except (TypeError, ValueError):
61
- return default
62
- if minimum is not None:
63
- value = max(minimum, value)
67
+ except ValueError:
68
+ raise ValueError(f"{name} must be an integer; got {raw!r}") from None
69
+ if minimum is not None and value < minimum:
70
+ raise ValueError(f"{name} must be >= {minimum}; got {value}")
64
71
  return value
65
72
 
66
73
 
@@ -72,18 +79,8 @@ def env_bool(name, default=False):
72
79
  return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
73
80
 
74
81
 
75
- def env_float(name, default, *, minimum=None):
76
- """Read a float env var; ignore malformed values instead of crashing import."""
77
- raw = os.environ.get(name)
78
- if raw is None or raw == "":
79
- return default
80
- try:
81
- value = float(raw)
82
- except (TypeError, ValueError):
83
- return default
84
- if minimum is not None:
85
- value = max(minimum, value)
86
- return value
82
+ def load_json(path):
83
+ return json.loads(Path(path).read_text(encoding="utf-8"))
87
84
 
88
85
 
89
86
  # Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
@@ -94,21 +91,21 @@ _mimo_api_key = os.environ.get("MIMO_API_KEY", "")
94
91
  _mimo_video_api_key = os.environ.get("MIMO_VIDEO_API_KEY", "") or _mimo_api_key
95
92
  _mimo_tts_api_key = os.environ.get("MIMO_TTS_API_KEY", "") or _mimo_api_key
96
93
  _mimo_asr_api_key = os.environ.get("MIMO_ASR_API_KEY", "") or _mimo_api_key
97
- _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
94
+ _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(is_mimo_token_plan_key(_mimo_api_key))
98
95
  _raw_mimo_video_api_url = (
99
96
  os.environ.get("MIMO_VIDEO_API_URL")
100
97
  or os.environ.get("MIMO_API_URL")
101
- or default_mimo_api_url(_mimo_video_api_key)
98
+ or default_mimo_api_url(is_mimo_token_plan_key(_mimo_video_api_key))
102
99
  )
103
100
  _raw_mimo_tts_api_url = (
104
101
  os.environ.get("MIMO_TTS_API_URL")
105
102
  or os.environ.get("MIMO_API_URL")
106
- or default_mimo_api_url(_mimo_tts_api_key)
103
+ or default_mimo_api_url(is_mimo_token_plan_key(_mimo_tts_api_key))
107
104
  )
108
105
  _raw_mimo_asr_api_url = (
109
106
  os.environ.get("MIMO_ASR_API_URL")
110
107
  or os.environ.get("MIMO_API_URL")
111
- or default_mimo_api_url(_mimo_asr_api_key)
108
+ or default_mimo_api_url(is_mimo_token_plan_key(_mimo_asr_api_key))
112
109
  )
113
110
 
114
111
  CONFIG = {
@@ -174,24 +171,6 @@ CONFIG = {
174
171
  "vlm_workers": env_int("VLM_WORKERS", 8, minimum=1), # VLM 并行分析线程数
175
172
  }
176
173
 
177
- def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
178
- """Return the canonical tempo budget shared by voiceover and assemble."""
179
- cfg = config or CONFIG
180
- global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
181
- rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
182
- cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
183
- hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
184
- legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
185
- segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
186
- return {
187
- "global_narration_speed": global_speed,
188
- "tts_rate_factor": rate_factor,
189
- "cumulative_tempo_max": cumulative_max,
190
- "cumulative_tempo_hard_max": hard_max,
191
- "segment_tempo_max": segment_tempo_max,
192
- "max_raw_duration_factor": global_speed * segment_tempo_max,
193
- }
194
-
195
174
 
196
175
  class MiMoQCRequestError(RuntimeError):
197
176
  """Sanitized, fail-open transport error for the advisory QC request."""
@@ -213,19 +192,18 @@ def mimo_qc_api_call(payload, *, config=None, timeout=60):
213
192
  endpoint = normalize_api_url(
214
193
  cfg.get("mimo_video_api_url") or cfg.get("mimo_api_url") or cfg.get("api_url")
215
194
  )
216
- body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
217
195
  request = urllib.request.Request(
218
196
  endpoint,
219
- data=body,
197
+ data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
220
198
  headers={
221
199
  "Content-Type": "application/json",
222
200
  "User-Agent": "video-recap/mimo-qc",
223
- "api-key": str(api_key),
201
+ "api-key": api_key,
224
202
  },
225
203
  method="POST",
226
204
  )
227
205
  try:
228
- with urllib.request.urlopen(request, timeout=float(timeout)) as response:
206
+ with urllib.request.urlopen(request, timeout=timeout) as response:
229
207
  raw = response.read().decode("utf-8")
230
208
  except urllib.error.HTTPError as exc:
231
209
  raise MiMoQCRequestError(f"http_{exc.code}") from None
@@ -235,7 +213,7 @@ def mimo_qc_api_call(payload, *, config=None, timeout=60):
235
213
  raise MiMoQCRequestError("network_error") from None
236
214
  try:
237
215
  result = json.loads(raw)
238
- except (TypeError, ValueError):
216
+ except ValueError:
239
217
  raise MiMoQCRequestError("invalid_json") from None
240
218
  if not isinstance(result, dict):
241
219
  raise MiMoQCRequestError("invalid_response")
@@ -23,6 +23,8 @@ import tempfile
23
23
  from datetime import datetime, timezone
24
24
  from pathlib import Path
25
25
 
26
+ from lib import load_json
27
+
26
28
  ALLOWED_ARTIFACTS = {
27
29
  "scenes.json",
28
30
  "asr_result.json",
@@ -106,11 +108,11 @@ def stable_json_dumps(value) -> str:
106
108
 
107
109
 
108
110
  def settings_fingerprint(settings) -> str:
109
- return hashlib.sha256(stable_json_dumps(settings or {}).encode("utf-8")).hexdigest()
111
+ return hashlib.sha256(stable_json_dumps(settings).encode("utf-8")).hexdigest()
110
112
 
111
113
 
112
114
  def source_id_from_fingerprint(fingerprint: str) -> str:
113
- return f"src_{str(fingerprint)[:12]}"
115
+ return f"src_{fingerprint[:12]}"
114
116
 
115
117
 
116
118
  def assign_source_ids(sources: list[dict]) -> list[dict]:
@@ -125,21 +127,16 @@ def assign_source_ids(sources: list[dict]) -> list[dict]:
125
127
  assigned = []
126
128
  for raw in sources:
127
129
  item = dict(raw)
128
- fp = str(item.get("source_video_fingerprint") or item.get("fingerprint") or "")
129
- if not fp:
130
- raise ValueError("source fingerprint is required")
130
+ fp = item["source_video_fingerprint"]
131
131
  base = source_id_from_fingerprint(fp)
132
- path = str(Path(item.get("source_path") or item.get("path") or "").resolve())
132
+ path = str(Path(item["source_path"]).resolve())
133
133
  used = seen.setdefault(fp, set())
134
- if not used:
135
- sid = base
136
- else:
137
- suffix = hashlib.sha256(path.encode("utf-8")).hexdigest()[:6]
138
- sid = f"{base}_{suffix}"
134
+ sid = base
135
+ if used:
136
+ sid = f"{base}_{hashlib.sha256(path.encode('utf-8')).hexdigest()[:6]}"
139
137
  # Avoid accidental path-hash collisions within one manifest.
140
138
  while sid in used:
141
- suffix = hashlib.sha256((path + sid).encode("utf-8")).hexdigest()[:6]
142
- sid = f"{base}_{suffix}"
139
+ sid = f"{base}_{hashlib.sha256((path + sid).encode('utf-8')).hexdigest()[:6]}"
143
140
  used.add(sid)
144
141
  item["source_id"] = sid
145
142
  item["source_path"] = path
@@ -148,34 +145,21 @@ def assign_source_ids(sources: list[dict]) -> list[dict]:
148
145
 
149
146
 
150
147
  def _slug(text: str, max_len: int = 48) -> str:
151
- raw = Path(str(text or "material")).stem.lower()
148
+ raw = Path(text).stem.lower()
152
149
  raw = re.sub(r"[^a-z0-9\u4e00-\u9fff._-]+", "-", raw).strip("-._")
153
150
  return (raw or "material")[:max_len].strip("-._") or "material"
154
151
 
155
152
 
156
153
  def material_id_for(source_path: str | Path, source_fingerprint: str) -> str:
157
- return f"{_slug(str(source_path))}-{str(source_fingerprint)[:12]}"
154
+ return f"{_slug(str(source_path))}-{source_fingerprint[:12]}"
158
155
 
159
156
 
160
157
  def material_dir(library_dir: str | Path, material_id: str) -> Path:
161
158
  return Path(library_dir) / "materials" / material_id
162
159
 
163
160
 
164
- def _load_json(path: Path):
165
- try:
166
- return json.loads(path.read_text(encoding="utf-8"))
167
- except (OSError, ValueError, TypeError):
168
- return None
169
-
170
-
171
- def _safe_text(value, limit: int = 600) -> str:
172
- text = str(value or "").replace("\x00", " ").strip()
173
- return _redact_text(text)[:limit]
174
-
175
-
176
161
  def _redact_text(text: str) -> str:
177
162
  """Redact credential value shapes only; leave ordinary words (secret/token/…) intact."""
178
- text = str(text or "")
179
163
  for rx in SECRET_VALUE_RES:
180
164
  text = rx.sub("[redacted-token]", text)
181
165
  return SECRET_ASSIGN_RE.sub(lambda m: f"{m.group(1)}{m.group(2)}[redacted-key]{m.group(4)}", text)
@@ -185,13 +169,12 @@ def _redact_json(value):
185
169
  if isinstance(value, dict):
186
170
  out = {}
187
171
  for key, item in value.items():
188
- key_text = str(key)
189
172
  # Drop only the value of an exact credential-named key; keep the key name and
190
173
  # never coalesce distinct keys (so benign fields like token_economy survive).
191
- if key_text.strip().lower() in SECRET_KEY_NAMES:
192
- out[key_text] = "[redacted]"
174
+ if key.strip().lower() in SECRET_KEY_NAMES:
175
+ out[key] = "[redacted]"
193
176
  else:
194
- out[key_text] = _redact_json(item)
177
+ out[key] = _redact_json(item)
195
178
  return out
196
179
  if isinstance(value, list):
197
180
  return [_redact_json(item) for item in value]
@@ -202,54 +185,36 @@ def _redact_json(value):
202
185
 
203
186
  def copy_artifact_redacted(src: Path, dst: Path) -> None:
204
187
  """Copy an allowed JSON/MD artifact without persisting obvious secret markers."""
205
- try:
206
- text = src.read_text(encoding="utf-8")
207
- except UnicodeDecodeError as exc:
208
- raise ValueError(f"refusing to persist non-text material artifact: {src.name}") from exc
188
+ text = src.read_text(encoding="utf-8")
209
189
  if src.suffix.lower() == ".json":
210
- try:
211
- data = json.loads(text)
212
- except ValueError:
213
- dst.write_text(_redact_text(text), encoding="utf-8")
214
- else:
215
- dst.write_text(json.dumps(_redact_json(data), ensure_ascii=False, indent=2), encoding="utf-8")
190
+ dst.write_text(json.dumps(_redact_json(json.loads(text)), ensure_ascii=False, indent=2), encoding="utf-8")
216
191
  else:
217
192
  dst.write_text(_redact_text(text), encoding="utf-8")
218
193
 
219
194
 
220
195
  def summarize_work_dir(work_dir: str | Path, *, source_name: str = "") -> dict:
221
- """Best-effort small summary for material.md/index grep."""
196
+ """Grep-friendly tags for material.md/index: character and entity names plus artifact counts."""
222
197
  work = Path(work_dir)
223
- summary_parts = []
198
+ summary = f"Analyzed video material: {source_name or work.name}"
224
199
  tags = []
225
- idx = _load_json(work / "understanding_index.json")
226
- if isinstance(idx, dict):
200
+ index_path = work / "understanding_index.json"
201
+ if index_path.exists():
202
+ index = load_json(index_path)
227
203
  for key in ("summary", "story_summary", "overall_summary", "one_sentence"):
228
- if idx.get(key):
229
- summary_parts.append(_safe_text(idx.get(key)))
204
+ if index.get(key):
205
+ summary = _redact_text(str(index[key]))[:600]
230
206
  break
231
207
  for key in ("characters", "entities", "keywords", "tags"):
232
- vals = idx.get(key)
233
- if isinstance(vals, list):
234
- tags.extend(_safe_text(v, 80) for v in vals[:12])
235
- vlm = _load_json(work / "vlm_analysis.json")
236
- if isinstance(vlm, dict):
237
- for key in ("summary", "overall_summary", "video_summary"):
238
- if vlm.get(key):
239
- summary_parts.append(_safe_text(vlm.get(key)))
240
- break
241
- scenes = _load_json(work / "scenes.json")
242
- if isinstance(scenes, list):
243
- tags.append(f"scenes:{len(scenes)}")
244
- asr = _load_json(work / "asr_result.json")
245
- if isinstance(asr, list):
246
- tags.append(f"asr:{len(asr)}")
247
- summary = " | ".join(p for p in summary_parts if p) or f"Analyzed video material: {source_name or work.name}"
248
- dedup_tags = []
249
- for tag in tags:
250
- if tag and tag not in dedup_tags:
251
- dedup_tags.append(tag)
252
- return {"summary": summary, "tags": dedup_tags[:20]}
208
+ for item in index.get(key, [])[:12]:
209
+ text = item.get("name", "") if isinstance(item, dict) else item
210
+ tags.append(_redact_text(str(text))[:80])
211
+ for name, label in (("scenes.json", "scenes"), ("asr_result.json", "asr")):
212
+ if (work / name).exists():
213
+ tags.append(f"{label}:{len(load_json(work / name))}")
214
+ return {
215
+ "summary": summary,
216
+ "tags": list(dict.fromkeys(tag for tag in tags if tag))[:20],
217
+ }
253
218
 
254
219
 
255
220
  def allowed_artifact_paths(work_dir: str | Path) -> list[Path]:
@@ -258,19 +223,19 @@ def allowed_artifact_paths(work_dir: str | Path) -> list[Path]:
258
223
 
259
224
 
260
225
  def write_material_md(path: Path, metadata: dict, summary: str, tags: list[str]) -> None:
261
- artifact_lines = "\n".join(f"- `{a.get('name')}` → `{a.get('path')}`" for a in metadata.get("artifacts", []))
226
+ artifact_lines = "\n".join(f"- `{a['name']}` → `{a['path']}`" for a in metadata["artifacts"])
262
227
  tags_text = ", ".join(tags) if tags else "(none)"
263
- text = f"""# Material: {metadata.get('source_name') or metadata.get('material_id')}
228
+ text = f"""# Material: {metadata['source_name']}
264
229
 
265
- - material_id: `{metadata.get('material_id')}`
266
- - source: `{metadata.get('source_path')}`
267
- - source_fingerprint: `{metadata.get('source_video_fingerprint')}`
268
- - settings_fingerprint: `{metadata.get('settings_fingerprint')}`
269
- - updated_at: `{metadata.get('updated_at')}`
230
+ - material_id: `{metadata['material_id']}`
231
+ - source: `{metadata['source_path']}`
232
+ - source_fingerprint: `{metadata['source_video_fingerprint']}`
233
+ - settings_fingerprint: `{metadata['settings_fingerprint']}`
234
+ - updated_at: `{metadata['updated_at']}`
270
235
  - tags: {tags_text}
271
236
 
272
237
  ## Summary
273
- {_safe_text(summary, 2000)}
238
+ {summary}
274
239
 
275
240
  ## Artifacts
276
241
  {artifact_lines or '- (none)'}
@@ -278,6 +243,23 @@ def write_material_md(path: Path, metadata: dict, summary: str, tags: list[str])
278
243
  path.write_text(text, encoding="utf-8")
279
244
 
280
245
 
246
+ def _read_material_metadata(path: Path) -> dict | None:
247
+ try:
248
+ data = load_json(path)
249
+ except (OSError, ValueError):
250
+ return None
251
+ return data if isinstance(data, dict) else None
252
+
253
+
254
+ def _material_cache_entry(path: Path) -> dict | None:
255
+ data = _read_material_metadata(path)
256
+ if not data or not all(data.get(key) for key in (
257
+ "source_video_fingerprint", "settings_fingerprint", "updated_at", "material_id",
258
+ )) or not isinstance(data.get("artifacts"), list):
259
+ return None
260
+ return data
261
+
262
+
281
263
  def save_material(
282
264
  library_dir: str | Path,
283
265
  work_dir: str | Path,
@@ -302,7 +284,7 @@ def save_material(
302
284
  fresh_names = {src.name for src in sources}
303
285
  # Reconcile: drop allowed artifacts left from a previous (larger) save so the on-disk
304
286
  # artifacts/ dir always matches material.json — a stale orphan must not surface in greps.
305
- for stale in artifacts_dir.iterdir() if artifacts_dir.exists() else []:
287
+ for stale in artifacts_dir.iterdir():
306
288
  if stale.is_file() and stale.name in ALLOWED_ARTIFACTS and stale.name not in fresh_names:
307
289
  stale.unlink()
308
290
  copied = []
@@ -311,8 +293,11 @@ def save_material(
311
293
  copy_artifact_redacted(src, dst)
312
294
  copied.append({"name": src.name, "path": f"artifacts/{src.name}", "sha256": file_fingerprint(dst)})
313
295
 
314
- existing = _load_json(dest / "material.json")
315
- created_at = existing.get("created_at") if isinstance(existing, dict) and existing.get("created_at") else now
296
+ meta_path = dest / "material.json"
297
+ previous = _read_material_metadata(meta_path)
298
+ created_at = previous.get("created_at") if previous else None
299
+ if not isinstance(created_at, str) or not created_at:
300
+ created_at = now
316
301
  source_path = Path(source_path).resolve()
317
302
  summary_info = summarize_work_dir(work_dir, source_name=source_path.name)
318
303
  metadata = {
@@ -328,7 +313,7 @@ def save_material(
328
313
  "created_at": created_at,
329
314
  "updated_at": now,
330
315
  }
331
- (dest / "material.json").write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
316
+ meta_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
332
317
  write_material_md(dest / "material.md", metadata, summary_info["summary"], summary_info["tags"])
333
318
 
334
319
  index_record = {
@@ -356,15 +341,15 @@ def find_material_by_fingerprint(library_dir: str | Path, source_fingerprint: st
356
341
  return None
357
342
  candidates = []
358
343
  for meta_path in root.glob("*/material.json"):
359
- data = _load_json(meta_path)
360
- if isinstance(data, dict) and data.get("source_video_fingerprint") == source_fingerprint:
344
+ data = _material_cache_entry(meta_path)
345
+ if data is not None and data["source_video_fingerprint"] == source_fingerprint:
361
346
  data["material_dir"] = str(meta_path.parent)
362
347
  candidates.append(data)
363
348
  if not candidates:
364
349
  return None
365
350
  # Deterministic fallback policy for legacy/manual callers that do not know
366
351
  # the expected material_id: newest wins, then material_id for stable ties.
367
- candidates.sort(key=lambda d: (str(d.get("updated_at") or ""), str(d.get("material_id") or "")), reverse=True)
352
+ candidates.sort(key=lambda d: (d["updated_at"], d["material_id"]), reverse=True)
368
353
  return candidates[0]
369
354
 
370
355
 
@@ -391,51 +376,43 @@ def restore_material(
391
376
  """
392
377
  lib = Path(library_dir)
393
378
  if material_id:
394
- meta = _load_json(material_dir(lib, material_id) / "material.json")
395
- if isinstance(meta, dict):
396
- meta["material_dir"] = str(material_dir(lib, material_id))
379
+ meta_path = material_dir(lib, material_id) / "material.json"
380
+ meta = _material_cache_entry(meta_path)
381
+ if meta is None:
382
+ return {"restored": False, "reason": "material missing or invalid"}
383
+ meta["material_dir"] = str(meta_path.parent)
397
384
  else:
398
385
  meta = find_material_by_fingerprint(lib, source_fingerprint)
399
- if not isinstance(meta, dict):
400
- return {"restored": False, "reason": "material not found"}
401
- if meta.get("source_video_fingerprint") != source_fingerprint:
402
- return {"restored": False, "reason": "source fingerprint mismatch", "material_id": meta.get("material_id")}
403
- if meta.get("settings_fingerprint") != settings_fp:
404
- return {"restored": False, "reason": "settings fingerprint mismatch", "material_id": meta.get("material_id")}
405
-
406
- src_dir = Path(meta.get("material_dir") or material_dir(lib, meta["material_id"])) / "artifacts"
386
+ if meta is None:
387
+ return {"restored": False, "reason": "material not found"}
388
+ if meta["source_video_fingerprint"] != source_fingerprint:
389
+ return {"restored": False, "reason": "source fingerprint mismatch", "material_id": meta["material_id"]}
390
+ if meta["settings_fingerprint"] != settings_fp:
391
+ return {"restored": False, "reason": "settings fingerprint mismatch", "material_id": meta["material_id"]}
392
+
393
+ src_dir = Path(meta["material_dir"]) / "artifacts"
407
394
  if not src_dir.exists():
408
- return {"restored": False, "reason": "material artifacts missing", "material_id": meta.get("material_id")}
395
+ return {"restored": False, "reason": "material artifacts missing", "material_id": meta["material_id"]}
409
396
  dest = Path(work_dir)
410
397
  dest.mkdir(parents=True, exist_ok=True)
411
- staged = []
412
398
  with tempfile.TemporaryDirectory(prefix=".material_restore_", dir=str(dest)) as tmp_name:
413
399
  tmp = Path(tmp_name)
414
- for artifact in meta.get("artifacts") or []:
415
- name = artifact.get("name") if isinstance(artifact, dict) else None
416
- if name not in ALLOWED_ARTIFACTS:
417
- continue
418
- src = src_dir / name
419
- if not src.exists():
420
- continue
421
- copy_artifact_redacted(src, tmp / name)
422
- staged.append(name)
423
- if not staged:
424
- return {
425
- "restored": False,
426
- "reason": "material artifacts empty",
427
- "material_id": meta.get("material_id"),
428
- }
400
+ staged = [
401
+ artifact["name"]
402
+ for artifact in meta["artifacts"]
403
+ if isinstance(artifact, dict) and artifact.get("name") in ALLOWED_ARTIFACTS
404
+ ]
405
+ if not staged or any(not (src_dir / name).is_file() for name in staged):
406
+ return {"restored": False, "reason": "material artifacts missing", "material_id": meta["material_id"]}
407
+ for name in staged:
408
+ copy_artifact_redacted(src_dir / name, tmp / name)
429
409
 
430
410
  pruned = []
431
411
  if prune_stale_allowed:
432
- staged_set = set(staged)
433
- for name in sorted(ALLOWED_ARTIFACTS):
434
- # Never prune an artifact we are about to restore: the restore loop below
435
- # honors `overwrite`, so pruning a staged name would (with overwrite=False)
436
- # delete it and then skip the copy, losing the file.
437
- if name in staged_set:
438
- continue
412
+ # Never prune an artifact we are about to restore: the restore loop below honors
413
+ # `overwrite`, so pruning a staged name would (with overwrite=False) delete it and
414
+ # then skip the copy, losing the file.
415
+ for name in sorted(ALLOWED_ARTIFACTS - set(staged)):
439
416
  out = dest / name
440
417
  if out.exists():
441
418
  out.unlink()
@@ -450,7 +427,7 @@ def restore_material(
450
427
  restored.append(name)
451
428
  return {
452
429
  "restored": bool(restored),
453
- "material_id": meta.get("material_id"),
430
+ "material_id": meta["material_id"],
454
431
  "artifacts": restored,
455
432
  "pruned_artifacts": pruned,
456
433
  "material": meta,
@@ -1,18 +1,13 @@
1
- """Load this skill's local MiMo client exactly once under an unambiguous name."""
1
+ """Load this skill's MiMo client without depending on the ambient ``lib`` module."""
2
2
 
3
3
  import importlib.util
4
4
  from pathlib import Path
5
5
 
6
- _LOCAL_LIB_PATH = Path(__file__).with_name("lib.py")
7
- _LOCAL_LIB_SPEC = importlib.util.spec_from_file_location(
8
- "video_recap_mimo_qc_lib", _LOCAL_LIB_PATH
6
+ _SPEC = importlib.util.spec_from_file_location(
7
+ "video_recap_mimo_qc_lib", Path(__file__).with_name("lib.py")
9
8
  )
10
- if (
11
- _LOCAL_LIB_SPEC is None or _LOCAL_LIB_SPEC.loader is None
12
- ): # pragma: no cover - import invariant
13
- raise ImportError(f"cannot load local MiMo QC client: {_LOCAL_LIB_PATH}")
14
- _LOCAL_LIB = importlib.util.module_from_spec(_LOCAL_LIB_SPEC)
15
- _LOCAL_LIB_SPEC.loader.exec_module(_LOCAL_LIB)
9
+ _LIB = importlib.util.module_from_spec(_SPEC)
10
+ _SPEC.loader.exec_module(_LIB)
16
11
 
17
- DEFAULT_CONFIG = _LOCAL_LIB.CONFIG
18
- mimo_qc_api_call = _LOCAL_LIB.mimo_qc_api_call
12
+ DEFAULT_CONFIG = _LIB.CONFIG
13
+ mimo_qc_api_call = _LIB.mimo_qc_api_call