@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
@@ -169,7 +169,7 @@ Resource-backed segments must define exactly one of:
169
169
  "resource_id": "...",
170
170
  "main_config": {"type": "sticker", "path": "/local/file"},
171
171
  "resources": [
172
- "/local/file",
172
+ {"source_path": "/local/file"},
173
173
  {"source_path": "/local/package-dir", "target_path": "package-dir"}
174
174
  ],
175
175
  "cover_img": "/local/cover.png",
@@ -180,14 +180,13 @@ Resource-backed segments must define exactly one of:
180
180
  ```
181
181
 
182
182
  `resource_package` resolves a top-level `resource_packages` entry with the same
183
- shape as `resource_config`. Both the local snake_case keys above and the real
184
- Jackson `JyResource` keys (`resourceId`, `mainConfig`, `coverImg`) are accepted.
185
- `main_config` / `mainConfig` may be an object or a JSON string;
186
- `resource_config` itself may be an object or a local JSON-file path.
183
+ canonical snake_case object shape as `resource_config`. `main_config` must be an
184
+ object; callers adapt any external Jackson `JyResource` or JSON-file input before
185
+ passing it to this skill.
187
186
 
188
187
  `resources` is an explicit local-file contract, never a filesystem guess. A
189
- string copies that file/directory using its basename. An object may additionally
190
- set `resource_kind` and a safe package-relative `target_path`. ZIP input is
188
+ descriptor must contain `source_path` and may additionally set `resource_kind`
189
+ and a safe package-relative `target_path`. ZIP input is
191
190
  validated against path traversal, extracted under
192
191
  `Resources/local/<kind>/<archive-stem>`, and keeps its internal layout. Exact
193
192
  declared path values in material JSON, including rich-text JSON strings, are
@@ -7,12 +7,10 @@ from __future__ import annotations
7
7
 
8
8
  import argparse
9
9
  import json
10
- import os
11
10
  import shutil
12
11
  import subprocess
13
12
  import sys
14
13
  from pathlib import Path
15
- from typing import Any, cast
16
14
 
17
15
  from lib import CONFIG
18
16
 
@@ -23,25 +21,17 @@ TTS_PROVIDERS = {"auto", "mimo-tts", "fish-audio"}
23
21
 
24
22
 
25
23
  def _command_path(name: str) -> str | None:
26
- """Return resolved command path, accepting absolute/relative executable paths."""
27
- if not name:
28
- return None
29
- if os.path.sep in name or (os.path.altsep and os.path.altsep in name):
30
- path = Path(name).expanduser()
31
- return str(path) if path.exists() and os.access(path, os.X_OK) else None
32
24
  return shutil.which(name)
33
25
 
34
26
 
35
- def _run(cmd: list[str], *, timeout: int = 20) -> subprocess.CompletedProcess[str]:
36
- return subprocess.run(cmd, text=True, capture_output=True, timeout=timeout)
37
-
38
-
39
27
  def _ffmpeg_filters() -> set[str]:
40
28
  ffmpeg = _command_path("ffmpeg")
41
29
  if not ffmpeg:
42
30
  return set()
43
31
  try:
44
- result = _run([ffmpeg, "-hide_banner", "-filters"], timeout=20)
32
+ result = subprocess.run(
33
+ [ffmpeg, "-hide_banner", "-filters"], text=True, capture_output=True, timeout=20
34
+ )
45
35
  except (OSError, subprocess.SubprocessError):
46
36
  return set()
47
37
  if result.returncode != 0:
@@ -64,15 +54,15 @@ def ffmpeg_has_subtitles_filter() -> bool:
64
54
 
65
55
 
66
56
  def _asr_status() -> dict[str, object]:
67
- configured = bool(CONFIG.get("mimo_asr_api_key"))
57
+ configured = bool(CONFIG["mimo_asr_api_key"])
68
58
  return {
69
59
  "configured": configured,
70
60
  "available": configured,
71
- "mimo_asr_model": str(CONFIG.get("mimo_asr_model") or ""),
72
- "mimo_asr_api_url": str(CONFIG.get("mimo_asr_api_url") or ""),
73
- "mimo_asr_api_url_source": CONFIG.get("mimo_asr_api_url_source", "default"),
74
- "mimo_asr_language": str(CONFIG.get("mimo_asr_language") or "auto"),
75
- "mimo_asr_api_key_source": CONFIG.get("mimo_asr_api_key_source", "MIMO_API_KEY"),
61
+ "mimo_asr_model": CONFIG["mimo_asr_model"],
62
+ "mimo_asr_api_url": CONFIG["mimo_asr_api_url"],
63
+ "mimo_asr_api_url_source": CONFIG["mimo_asr_api_url_source"],
64
+ "mimo_asr_language": CONFIG["mimo_asr_language"],
65
+ "mimo_asr_api_key_source": CONFIG["mimo_asr_api_key_source"],
76
66
  "note": "ASR uses MiMo (mimo-v2.5-asr); set MIMO_API_KEY, or run with --skip-asr.",
77
67
  }
78
68
 
@@ -86,16 +76,16 @@ def _capability(name: str, summary: str, *, detail: str = "", action: str = "")
86
76
  return item
87
77
 
88
78
 
89
- def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str, str]]]:
79
+ def _build_capability_menu(checks: dict) -> dict[str, list[dict[str, str]]]:
90
80
  """Human-ready preflight summary grouped by what can run, what blocks, and what degrades.
91
81
 
92
82
  This is intentionally a small rollup over the existing `checks` tree. It does not replace
93
83
  the raw machine checks, install anything, or introduce provider ranking.
94
84
  """
95
- system = cast(dict[str, Any], checks["system_tools"])
96
- api = cast(dict[str, Any], checks["api_config"])
97
- asr = cast(dict[str, Any], checks["asr"])
98
- tts = cast(dict[str, Any], checks["tts"])
85
+ system = checks["system_tools"]
86
+ api = checks["api_config"]
87
+ asr = checks["asr"]
88
+ tts = checks["tts"]
99
89
 
100
90
  menu: dict[str, list[dict[str, str]]] = {
101
91
  "ready": [],
@@ -104,13 +94,13 @@ def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str
104
94
  "optional_upgrades": [],
105
95
  }
106
96
 
107
- ffmpeg_ready = bool(system.get("ffmpeg"))
108
- ffprobe_ready = bool(system.get("ffprobe"))
109
- subtitles_ready = bool(system.get("burn_subtitles_ready"))
110
- api_key_set = bool(api.get("api_key_set"))
111
- asr_ready = bool(asr.get("available"))
112
- tts_ready = bool(tts.get("available"))
113
- vlm_ready = bool(api.get("mimo_video_configured"))
97
+ ffmpeg_ready = system["ffmpeg"]
98
+ ffprobe_ready = system["ffprobe"]
99
+ subtitles_ready = system["burn_subtitles_ready"]
100
+ api_key_set = api["api_key_set"]
101
+ asr_ready = asr["available"]
102
+ tts_ready = tts["available"]
103
+ vlm_ready = api["mimo_video_configured"]
114
104
  normal_core_ready = ffmpeg_ready and ffprobe_ready and api_key_set and vlm_ready and tts_ready
115
105
 
116
106
  if ffmpeg_ready and ffprobe_ready:
@@ -136,7 +126,7 @@ def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str
136
126
  _capability(
137
127
  "mimo_credentials",
138
128
  "MiMo API key is configured",
139
- detail=f"Source: {api.get('api_key_source')}",
129
+ detail=f"Source: {api['api_key_source']}",
140
130
  )
141
131
  )
142
132
  else:
@@ -153,7 +143,7 @@ def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str
153
143
  _capability(
154
144
  "mimo_vlm",
155
145
  "MiMo VLM/video understanding is configured",
156
- detail=f"Model: {api.get('vlm_model')}",
146
+ detail=f"Model: {api['vlm_model']}",
157
147
  )
158
148
  )
159
149
  elif api_key_set:
@@ -165,13 +155,13 @@ def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str
165
155
  )
166
156
  )
167
157
 
168
- tts_provider = str(tts.get("provider") or "mimo-tts")
158
+ tts_provider = tts["provider"]
169
159
  if tts_ready:
170
160
  menu["ready"].append(
171
161
  _capability(
172
162
  "fish_audio_tts" if tts_provider == "fish-audio" else "mimo_tts",
173
163
  f"{tts_provider} is configured",
174
- detail=f"Model: {tts.get('model')}",
164
+ detail=f"Model: {tts['model']}",
175
165
  )
176
166
  )
177
167
  elif api_key_set:
@@ -192,7 +182,7 @@ def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str
192
182
  _capability(
193
183
  "mimo_asr",
194
184
  "MiMo ASR is configured",
195
- detail=f"Language: {asr.get('mimo_asr_language')}; model: {asr.get('mimo_asr_model')}",
185
+ detail=f"Language: {asr['mimo_asr_language']}; model: {asr['mimo_asr_model']}",
196
186
  )
197
187
  )
198
188
  else:
@@ -200,7 +190,7 @@ def _build_capability_menu(checks: dict[str, object]) -> dict[str, list[dict[str
200
190
  _capability(
201
191
  "mimo_asr",
202
192
  "ASR is unavailable; run only with --skip-asr",
203
- action=str(asr.get("note") or "Set MIMO_ASR_API_KEY or MIMO_API_KEY to enable ASR."),
193
+ action=asr["note"],
204
194
  )
205
195
  )
206
196
 
@@ -266,12 +256,10 @@ def build_report(*, tts_provider: str | None = None) -> dict[str, object]:
266
256
  filters = _ffmpeg_filters()
267
257
  ffmpeg_path = _command_path("ffmpeg") or ""
268
258
  ffprobe_path = _command_path("ffprobe") or ""
269
- mimo_video_configured = bool(CONFIG.get("mimo_video_api_key"))
270
- mimo_tts_configured = bool(CONFIG.get("mimo_tts_api_key"))
271
- fish_tts_configured = bool(CONFIG.get("fish_api_key"))
272
- requested_tts_provider = str(
273
- tts_provider or CONFIG.get("tts_provider") or "auto"
274
- ).strip().lower()
259
+ mimo_video_configured = bool(CONFIG["mimo_video_api_key"])
260
+ mimo_tts_configured = bool(CONFIG["mimo_tts_api_key"])
261
+ fish_tts_configured = bool(CONFIG["fish_api_key"])
262
+ requested_tts_provider = tts_provider or CONFIG["tts_provider"]
275
263
  effective_tts_provider = requested_tts_provider
276
264
  if requested_tts_provider == "auto":
277
265
  effective_tts_provider = (
@@ -281,55 +269,52 @@ def build_report(*, tts_provider: str | None = None) -> dict[str, object]:
281
269
  fish_tts_configured if effective_tts_provider == "fish-audio" else mimo_tts_configured
282
270
  )
283
271
  subtitle_filter = "subtitles" in filters
284
- ass_filter = "ass" in filters
285
- checks: dict[str, object] = {
272
+ checks = {
286
273
  "system_tools": {
287
274
  "ffmpeg": bool(ffmpeg_path),
288
275
  "ffmpeg_path": ffmpeg_path,
289
276
  "ffprobe": bool(ffprobe_path),
290
277
  "ffprobe_path": ffprobe_path,
291
278
  "ffmpeg_subtitles_filter": subtitle_filter,
292
- "ffmpeg_ass_filter": ass_filter,
279
+ "ffmpeg_ass_filter": "ass" in filters,
293
280
  "burn_subtitles_ready": bool(ffmpeg_path and subtitle_filter),
294
281
  },
295
282
  "tts": {
296
283
  "provider": effective_tts_provider,
297
284
  "requested_provider": requested_tts_provider,
298
285
  "mimo_tts_configured": mimo_tts_configured,
299
- "mimo_tts_api_url": CONFIG.get("mimo_tts_api_url"),
300
- "mimo_tts_api_url_source": CONFIG.get("mimo_tts_api_url_source", "default"),
301
- "mimo_tts_model": CONFIG.get("mimo_tts_model"),
302
- "mimo_tts_model_source": CONFIG.get("mimo_tts_model_source", "default"),
303
- "mimo_tts_voice": CONFIG.get("mimo_tts_voice"),
304
- "mimo_tts_voice_source": CONFIG.get("mimo_tts_voice_source", "default"),
286
+ "mimo_tts_api_url": CONFIG["mimo_tts_api_url"],
287
+ "mimo_tts_api_url_source": CONFIG["mimo_tts_api_url_source"],
288
+ "mimo_tts_model": CONFIG["mimo_tts_model"],
289
+ "mimo_tts_model_source": CONFIG["mimo_tts_model_source"],
290
+ "mimo_tts_voice": CONFIG["mimo_tts_voice"],
291
+ "mimo_tts_voice_source": CONFIG["mimo_tts_voice_source"],
305
292
  "fish_tts_configured": fish_tts_configured,
306
- "fish_tts_api_url": CONFIG.get("fish_tts_api_url"),
307
- "fish_tts_model": CONFIG.get("fish_tts_model"),
308
- "fish_tts_reference_id_set": bool(CONFIG.get("fish_tts_reference_id")),
309
- "fish_tts_reference_id_source": CONFIG.get(
310
- "fish_tts_reference_id_source", "default"
311
- ),
293
+ "fish_tts_api_url": CONFIG["fish_tts_api_url"],
294
+ "fish_tts_model": CONFIG["fish_tts_model"],
295
+ "fish_tts_reference_id_set": bool(CONFIG["fish_tts_reference_id"]),
296
+ "fish_tts_reference_id_source": CONFIG["fish_tts_reference_id_source"],
312
297
  "model": (
313
- CONFIG.get("fish_tts_model")
298
+ CONFIG["fish_tts_model"]
314
299
  if effective_tts_provider == "fish-audio"
315
- else CONFIG.get("mimo_tts_model")
300
+ else CONFIG["mimo_tts_model"]
316
301
  ),
317
302
  "available": tts_configured,
318
303
  },
319
304
  "asr": _asr_status(),
320
305
  "api_config": {
321
- "api_provider": CONFIG.get("api_provider", "mimo"),
322
- "api_url": str(CONFIG.get("api_url") or ""),
323
- "api_url_source": CONFIG.get("api_url_source", "default"),
324
- "api_key_source": CONFIG.get("api_key_source", "MIMO_API_KEY"),
325
- "api_key_set": bool(CONFIG.get("api_key")),
326
- "vlm_model": CONFIG.get("vlm_model"),
327
- "vlm_model_source": CONFIG.get("vlm_model_source", "default"),
328
- "vlm_workers": CONFIG.get("vlm_workers"),
306
+ "api_provider": CONFIG["api_provider"],
307
+ "api_url": CONFIG["api_url"],
308
+ "api_url_source": CONFIG["api_url_source"],
309
+ "api_key_source": CONFIG["api_key_source"],
310
+ "api_key_set": bool(CONFIG["api_key"]),
311
+ "vlm_model": CONFIG["vlm_model"],
312
+ "vlm_model_source": CONFIG["vlm_model_source"],
313
+ "vlm_workers": CONFIG["vlm_workers"],
329
314
  "mimo_video_configured": mimo_video_configured,
330
- "mimo_video_api_url": CONFIG.get("mimo_video_api_url"),
331
- "mimo_video_model": CONFIG.get("mimo_video_model"),
332
- "mimo_video_model_source": CONFIG.get("mimo_video_model_source", "default"),
315
+ "mimo_video_api_url": CONFIG["mimo_video_api_url"],
316
+ "mimo_video_model": CONFIG["mimo_video_model"],
317
+ "mimo_video_model_source": CONFIG["mimo_video_model_source"],
333
318
  },
334
319
  "python": {
335
320
  "executable": sys.executable,
@@ -339,28 +324,25 @@ def build_report(*, tts_provider: str | None = None) -> dict[str, object]:
339
324
 
340
325
  failures: list[str] = []
341
326
  warnings: list[str] = []
342
- tools = cast(dict[str, Any], checks["system_tools"])
343
- api_config = cast(dict[str, Any], checks["api_config"])
344
- asr_check = cast(dict[str, Any], checks["asr"])
327
+ tools = checks["system_tools"]
345
328
  for name in ("ffmpeg", "ffprobe"):
346
- if not tools.get(name):
329
+ if not tools[name]:
347
330
  failures.append(f"Missing system tool: {name}")
348
331
  if requested_tts_provider not in TTS_PROVIDERS:
349
332
  failures.append(
350
333
  "TTS_PROVIDER must be one of: auto, mimo-tts, fish-audio"
351
334
  )
352
- if tools.get("ffmpeg") and not tools.get("ffmpeg_subtitles_filter"):
335
+ if tools["ffmpeg"] and not tools["ffmpeg_subtitles_filter"]:
353
336
  warnings.append("ffmpeg lacks subtitles/libass filter; --burn-subtitles will fail")
354
- if not api_config.get("api_key_set"):
337
+ if not checks["api_config"]["api_key_set"]:
355
338
  failures.append("MIMO_API_KEY is not set; the default ASR / VLM path requires MiMo")
356
- if not asr_check.get("available"):
339
+ if not checks["asr"]["available"]:
357
340
  warnings.append("ASR not configured (MIMO_API_KEY); pipeline can run with --skip-asr")
358
- capability_menu = _build_capability_menu(checks)
359
341
  return {
360
342
  "ok": not failures,
361
343
  "repo_root": str(SCRIPT_DIR.parents[2]),
362
344
  "checks": checks,
363
- "capability_menu": capability_menu,
345
+ "capability_menu": _build_capability_menu(checks),
364
346
  "failures": failures,
365
347
  "warnings": warnings,
366
348
  }
@@ -372,88 +354,86 @@ def _status_icon(ok: bool, *, warning: bool = False) -> str:
372
354
  return "!" if warning else "✗"
373
355
 
374
356
 
375
- def _print_human(report: dict[str, object]) -> None:
376
- checks = cast(dict[str, Any], report["checks"])
357
+ def _print_human(report: dict) -> None:
358
+ checks = report["checks"]
377
359
  print("video-recap doctor")
378
360
  print(f"Repo root: {report['repo_root']}")
379
361
 
380
- system = cast(dict[str, Any], checks["system_tools"])
362
+ system = checks["system_tools"]
381
363
  print("\n[system]")
382
- print(f"{_status_icon(bool(system.get('ffmpeg')))} ffmpeg: {system.get('ffmpeg_path') or 'not found'}")
383
- print(f"{_status_icon(bool(system.get('ffprobe')))} ffprobe: {system.get('ffprobe_path') or 'not found'}")
364
+ print(f"{_status_icon(system['ffmpeg'])} ffmpeg: {system['ffmpeg_path'] or 'not found'}")
365
+ print(f"{_status_icon(system['ffprobe'])} ffprobe: {system['ffprobe_path'] or 'not found'}")
384
366
  print(
385
- f"{_status_icon(bool(system.get('ffmpeg_subtitles_filter')), warning=True)} "
367
+ f"{_status_icon(system['ffmpeg_subtitles_filter'], warning=True)} "
386
368
  f"ffmpeg subtitles/libass filter: "
387
- f"{'available' if system.get('ffmpeg_subtitles_filter') else 'missing'}"
369
+ f"{'available' if system['ffmpeg_subtitles_filter'] else 'missing'}"
388
370
  )
389
371
 
390
- api = cast(dict[str, Any], checks["api_config"])
372
+ api = checks["api_config"]
391
373
  print("\n[api]")
392
- print(f"✓ API provider: {api.get('api_provider')}")
393
- print(f"✓ API URL: {api.get('api_url')} (source: {api.get('api_url_source')})")
374
+ print(f"✓ API provider: {api['api_provider']}")
375
+ print(f"✓ API URL: {api['api_url']} (source: {api['api_url_source']})")
394
376
  print(
395
- f"{_status_icon(bool(api.get('api_key_set')))} "
396
- f"{api.get('api_key_source')}: {'set' if api.get('api_key_set') else 'not set'}"
377
+ f"{_status_icon(api['api_key_set'])} "
378
+ f"{api['api_key_source']}: {'set' if api['api_key_set'] else 'not set'}"
397
379
  )
398
- print(f"✓ VLM model: {api.get('vlm_model')} (source: {api.get('vlm_model_source')})")
399
- print(f"✓ VLM_WORKERS: {api.get('vlm_workers')}")
380
+ print(f"✓ VLM model: {api['vlm_model']} (source: {api['vlm_model_source']})")
381
+ print(f"✓ VLM_WORKERS: {api['vlm_workers']}")
400
382
 
401
- asr = cast(dict[str, Any], checks["asr"])
383
+ asr = checks["asr"]
402
384
  print("\n[asr]")
403
385
  print(
404
- f"{_status_icon(bool(asr.get('available')), warning=True)} "
405
- f"MiMo ASR: {'configured' if asr.get('available') else 'not configured'} "
406
- f"(key: {asr.get('mimo_asr_api_key_source')})"
386
+ f"{_status_icon(asr['available'], warning=True)} "
387
+ f"MiMo ASR: {'configured' if asr['available'] else 'not configured'} "
388
+ f"(key: {asr['mimo_asr_api_key_source']})"
407
389
  )
408
- print(f"✓ ASR model: {asr.get('mimo_asr_model')}")
409
- print(f"✓ ASR API URL: {asr.get('mimo_asr_api_url')} (source: {asr.get('mimo_asr_api_url_source')})")
410
- print(f"✓ ASR language: {asr.get('mimo_asr_language')}")
411
- if not asr.get("available"):
412
- print(f" note: {asr.get('note')}")
390
+ print(f"✓ ASR model: {asr['mimo_asr_model']}")
391
+ print(f"✓ ASR API URL: {asr['mimo_asr_api_url']} (source: {asr['mimo_asr_api_url_source']})")
392
+ print(f"✓ ASR language: {asr['mimo_asr_language']}")
393
+ if not asr["available"]:
394
+ print(f" note: {asr['note']}")
413
395
 
414
- tts = cast(dict[str, Any], checks["tts"])
396
+ tts = checks["tts"]
415
397
  print("\n[tts]")
416
398
  print(
417
- f"{_status_icon(bool(tts.get('available')))} {tts.get('provider')}: "
418
- f"{'configured' if tts.get('available') else 'not configured'}"
399
+ f"{_status_icon(tts['available'])} {tts['provider']}: "
400
+ f"{'configured' if tts['available'] else 'not configured'}"
419
401
  )
420
- print(f"✓ TTS model: {tts.get('model')}")
421
- if tts.get("provider") == "fish-audio":
402
+ print(f"✓ TTS model: {tts['model']}")
403
+ if tts["provider"] == "fish-audio":
422
404
  print(
423
405
  "✓ TTS voice reference ID: "
424
- f"{'set' if tts.get('fish_tts_reference_id_set') else 'not set'} "
425
- f"(source: {tts.get('fish_tts_reference_id_source')})"
406
+ f"{'set' if tts['fish_tts_reference_id_set'] else 'not set'} "
407
+ f"(source: {tts['fish_tts_reference_id_source']})"
426
408
  )
427
- print(f"✓ TTS API URL: {tts.get('fish_tts_api_url')}")
409
+ print(f"✓ TTS API URL: {tts['fish_tts_api_url']}")
428
410
  else:
429
- print(f"✓ TTS voice: {tts.get('mimo_tts_voice')} (source: {tts.get('mimo_tts_voice_source')})")
430
- print(f"✓ TTS API URL: {tts.get('mimo_tts_api_url')} (source: {tts.get('mimo_tts_api_url_source')})")
411
+ print(f"✓ TTS voice: {tts['mimo_tts_voice']} (source: {tts['mimo_tts_voice_source']})")
412
+ print(f"✓ TTS API URL: {tts['mimo_tts_api_url']} (source: {tts['mimo_tts_api_url_source']})")
431
413
 
432
- menu = cast(dict[str, list[dict[str, str]]], report.get("capability_menu") or {})
414
+ menu = report["capability_menu"]
433
415
  print("\n[capability menu]")
434
416
  for group in ("ready", "blocked", DEGRADED_GROUP, "optional_upgrades"):
435
417
  print(f"{group}:")
436
- items = menu.get(group) or []
418
+ items = menu[group]
437
419
  if not items:
438
420
  print(" - none")
439
421
  continue
440
422
  for item in items:
441
- line = f" - {item.get('name')}: {item.get('summary')}"
442
- if item.get("detail"):
423
+ line = f" - {item['name']}: {item['summary']}"
424
+ if "detail" in item:
443
425
  line += f" ({item['detail']})"
444
426
  print(line)
445
- if item.get("action"):
427
+ if "action" in item:
446
428
  print(f" action: {item['action']}")
447
429
 
448
- warnings = cast(list[str], report.get("warnings") or [])
449
- failures = cast(list[str], report.get("failures") or [])
450
- if warnings:
430
+ if report["warnings"]:
451
431
  print("\nWarnings:")
452
- for warning in warnings:
432
+ for warning in report["warnings"]:
453
433
  print(f"- {warning}")
454
- if failures:
434
+ if report["failures"]:
455
435
  print("\nStatus: FAILED")
456
- for failure in failures:
436
+ for failure in report["failures"]:
457
437
  print(f"- {failure}")
458
438
  else:
459
439
  print("\nStatus: OK")