@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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  """video-recap inspect — advisory, read-only orientation over a recap work_dir.
3
3
 
4
- Pure stdlib. Reads ONLY the JSON artifacts already in work_dir (no ffmpeg, no frame
5
- reads, no video probing, no new deps, no cross-skill import). Every missing or malformed
6
- artifact degrades to a clear human message — never a traceback.
4
+ Reads ONLY the JSON artifacts already in work_dir (no ffmpeg, no frame reads, no video
5
+ probing, no new deps, no cross-skill import). A missing artifact is the legitimate
6
+ "that stage has not run yet" state and is reported as such.
7
7
 
8
8
  Two subcommands:
9
9
  state summarize the work_dir: source video + fingerprint, full|cut mode, which stage
@@ -22,6 +22,10 @@ import json
22
22
  from pathlib import Path
23
23
 
24
24
 
25
+ def load_json(path):
26
+ return json.loads(Path(path).read_text(encoding="utf-8"))
27
+
28
+
25
29
  # --- artifact catalog --------------------------------------------------------
26
30
  # Stage artifacts probed by `state`. Order = rough pipeline order.
27
31
  _UNDERSTANDING_ARTIFACTS = [
@@ -57,7 +61,6 @@ _STORYBOARD_ARTIFACTS = [
57
61
  "storyboard/source_storyboard.json",
58
62
  "storyboard/edited_storyboard.json",
59
63
  ]
60
- # Forward-compat: if a write-side state file ever lands, prefer it as the state source.
61
64
  _FORWARD_STATE_FILES = ["manifest.json", "task_state.json"]
62
65
 
63
66
  _COMPACT_TEXT_LIMIT = 80
@@ -70,88 +73,71 @@ def _truncate(text, compact):
70
73
  return text[: _COMPACT_TEXT_LIMIT - 1] + "…"
71
74
 
72
75
 
73
- def _load_json(path):
74
- """Return (data, error). error is a human string when the file is missing/malformed."""
76
+ def _load_optional(path):
77
+ """The artifact's JSON, or None when the stage that writes it has not run yet."""
75
78
  path = Path(path)
76
79
  if not path.exists():
77
- return None, f"{path.name} 不存在"
80
+ return None
78
81
  try:
79
- return json.loads(path.read_text(encoding="utf-8")), None
80
- except ValueError:
81
- return None, f"{path.name} 不是合法 JSON(可能写坏了)"
82
- except OSError as exc:
83
- return None, f"{path.name} 读取失败: {exc}"
82
+ return load_json(path)
83
+ except (OSError, ValueError):
84
+ return None
84
85
 
85
86
 
86
87
  def _fmt_seconds(value):
87
- """mm:ss for a numeric seconds value, or the raw value when not numeric."""
88
- try:
89
- total = float(value)
90
- except (TypeError, ValueError):
91
- return str(value)
92
- sign = "-" if total < 0 else ""
93
- total = abs(total)
94
- m = int(total // 60)
95
- s = total % 60
96
- return f"{sign}{m:02d}:{s:05.2f}"
88
+ """mm:ss.ss for a seconds value."""
89
+ sign = "-" if value < 0 else ""
90
+ total = abs(value)
91
+ return f"{sign}{int(total // 60):02d}:{total % 60:05.2f}"
97
92
 
98
93
 
99
94
  # --- source video discovery --------------------------------------------------
100
95
  def _discover_source(work_dir):
101
96
  """Find the source video path + fingerprint by reading recap artifacts, most authoritative
102
- first. Returns a dict {path, fingerprint, origin} with None values + origin="unknown" when
103
- nothing records it. Never raises."""
97
+ first. Returns {path, fingerprint, origin} with None values + origin="unknown" when
98
+ nothing records it."""
99
+ work_dir = Path(work_dir)
104
100
  # 1. recap_run_manifest.json — canonical: written before the first pause with both fields.
105
- data, _ = _load_json(Path(work_dir) / "recap_run_manifest.json")
101
+ # A multi-source manifest carries `sources` instead of `source_video`.
102
+ data = _load_optional(work_dir / "recap_run_manifest.json")
106
103
  if isinstance(data, dict) and data.get("source_video"):
107
104
  return {
108
- "path": data.get("source_video"),
105
+ "path": data["source_video"],
109
106
  "fingerprint": data.get("source_video_fingerprint"),
110
107
  "origin": "recap_run_manifest.json",
111
108
  }
112
109
  # 2. assembly_manifest.json — late stage; carries input_video (+ source_video in cut mode).
113
- data, _ = _load_json(Path(work_dir) / "assembly_manifest.json")
110
+ data = _load_optional(work_dir / "assembly_manifest.json")
114
111
  if isinstance(data, dict) and (data.get("source_video") or data.get("input_video")):
115
112
  return {
116
- "path": data.get("source_video") or data.get("input_video"),
113
+ "path": data.get("source_video") or data["input_video"],
117
114
  "fingerprint": data.get("source_video_fingerprint"),
118
115
  "origin": "assembly_manifest.json",
119
116
  }
120
- # 3. edited_source.mp4.meta.json — cut mode; fingerprint only, no path.
121
- data, _ = _load_json(Path(work_dir) / "edited_source.mp4.meta.json")
122
- if isinstance(data, dict) and data.get("source_video_fingerprint"):
117
+ # 3. edited_source.mp4.meta.json — single-source cut; fingerprint only, no path.
118
+ data = _load_optional(work_dir / "edited_source.mp4.meta.json")
119
+ if data is not None and data.get("source_video_fingerprint"):
123
120
  return {
124
121
  "path": None,
125
- "fingerprint": data.get("source_video_fingerprint"),
122
+ "fingerprint": data["source_video_fingerprint"],
126
123
  "origin": "edited_source.mp4.meta.json",
127
124
  }
128
125
  return {"path": None, "fingerprint": None, "origin": "unknown"}
129
126
 
130
127
 
131
128
  def _discover_multi_source(work_dir):
132
- data, _ = _load_json(Path(work_dir) / "multi_source_manifest.json")
129
+ data = _load_optional(Path(work_dir) / "multi_source_manifest.json")
133
130
  if not isinstance(data, dict) or not isinstance(data.get("sources"), list):
134
131
  return None
135
- sources = []
136
- for raw in data.get("sources") or []:
137
- if not isinstance(raw, dict):
138
- continue
139
- sources.append({
140
- "source_id": raw.get("source_id"),
141
- "source_path": raw.get("source_path"),
142
- "source_name": raw.get("source_name"),
143
- "source_video_fingerprint": raw.get("source_video_fingerprint"),
144
- "source_work_dir": raw.get("source_work_dir"),
145
- "material_id": raw.get("material_id"),
146
- })
147
- return {"schema_version": data.get("schema_version", 1), "sources": sources}
148
-
149
-
150
- # --- clip plan loading + forward affine map ----------------------------------
132
+ return {
133
+ "schema_version": data.get("schema_version", 1),
134
+ "sources": [source for source in data["sources"] if isinstance(source, dict)],
135
+ }
136
+
137
+
151
138
  def _clip_entries(plan):
152
- """Normalize a clip plan (dict-with-clips or bare list) to the list of clip dicts."""
153
139
  if isinstance(plan, dict):
154
- entries = plan.get("clips", [])
140
+ entries = plan.get("clips")
155
141
  elif isinstance(plan, list):
156
142
  entries = plan
157
143
  else:
@@ -160,57 +146,53 @@ def _clip_entries(plan):
160
146
 
161
147
 
162
148
  def _normalize_clips(entries):
163
- """Coerce raw clip entries to {clip_id, source_start, source_end, output_start, output_end}.
164
-
165
- Mirrors assemble._output_clip_spans / _build_video_clips field access: source_start/source_end
166
- fall back to start/end; when output_start/output_end are absent they are derived with the same
167
- forward cursor cut.py uses (sum of prior kept-clip durations). Bad rows are skipped, not fatal.
168
- """
149
+ """Normalize current and historical validated plans for the advisory mapper."""
169
150
  clips, cursor = [], 0.0
170
- for idx, c in enumerate(entries or []):
171
- if not isinstance(c, dict):
151
+ for index, raw in enumerate(entries):
152
+ if not isinstance(raw, dict):
172
153
  continue
173
154
  try:
174
- ss = float(c.get("source_start", c.get("start")))
175
- se = float(c.get("source_end", c.get("end")))
155
+ source_start = float(raw.get("source_start", raw.get("start")))
156
+ source_end = float(raw.get("source_end", raw.get("end")))
176
157
  except (TypeError, ValueError):
177
158
  continue
178
- if se - ss <= 0:
159
+ if source_end <= source_start:
179
160
  continue
180
- out_s, out_e = c.get("output_start"), c.get("output_end")
181
- if out_s is None or out_e is None:
182
- out_s, out_e = cursor, cursor + (se - ss)
183
- cursor += se - ss
161
+ output_start = raw.get("output_start")
162
+ output_end = raw.get("output_end")
163
+ if output_start is None or output_end is None:
164
+ output_start, output_end = cursor, cursor + source_end - source_start
184
165
  else:
185
166
  try:
186
- out_s, out_e = float(out_s), float(out_e)
167
+ output_start, output_end = float(output_start), float(output_end)
187
168
  except (TypeError, ValueError):
188
- out_s, out_e = cursor, cursor + (se - ss)
189
- cursor += se - ss
190
- else:
191
- cursor = max(cursor, out_e)
192
- clips.append({
193
- "clip_id": c.get("clip_id", idx),
194
- "source_id": c.get("source_id"),
195
- "source_path": c.get("source_path"),
196
- "source_start": ss,
197
- "source_end": se,
198
- "output_start": out_s,
199
- "output_end": out_e,
200
- "reason": str(c.get("reason", c.get("note", ""))).strip(),
201
- })
169
+ continue
170
+ cursor = output_end
171
+ clips.append(
172
+ {
173
+ "clip_id": raw.get("clip_id", index),
174
+ "source_id": raw.get("source_id"),
175
+ "source_path": raw.get("source_path"),
176
+ "source_start": source_start,
177
+ "source_end": source_end,
178
+ "output_start": output_start,
179
+ "output_end": output_end,
180
+ "reason": str(raw.get("reason", raw.get("note", ""))).strip(),
181
+ }
182
+ )
202
183
  return clips
203
184
 
204
185
 
186
+ # --- forward affine map --------------------------------------------------------
205
187
  def _source_to_output(src, clip):
206
- """The forward affine map cut.py:319 uses, clamped to the clip's output span."""
207
- mapped = clip["output_start"] + (float(src) - clip["source_start"])
188
+ """The forward affine map cut.py uses, clamped to the clip's output span."""
189
+ mapped = clip["output_start"] + (src - clip["source_start"])
208
190
  return round(max(clip["output_start"], min(mapped, clip["output_end"])), 3)
209
191
 
210
192
 
211
193
  def _output_to_source(out, clip):
212
194
  """Inverse of the forward affine map (same slope, clamped to the clip's source span)."""
213
- mapped = clip["source_start"] + (float(out) - clip["output_start"])
195
+ mapped = clip["source_start"] + (out - clip["output_start"])
214
196
  return round(max(clip["source_start"], min(mapped, clip["source_end"])), 3)
215
197
 
216
198
 
@@ -255,20 +237,14 @@ def _next_pause(work_dir, mode):
255
237
 
256
238
 
257
239
  def _stale_manifest_note(work_dir, mode):
258
- """Advisory stale-manifest risks read purely from JSON (no fingerprint recompute — that would
259
- need the source bytes). Surfaces the two desync traps recap.py guards: a cut narration written
260
- against an older clip_plan, and a missing run manifest."""
240
+ """Advisory stale-manifest risks read purely from file presence (no fingerprint recompute —
241
+ that would need the source bytes). Surfaces the two desync traps recap.py guards: a cut
242
+ narration without the phase ledger that ties it to a clip_plan, and a missing run manifest."""
261
243
  notes = []
262
244
  if not _present(work_dir, "recap_run_manifest.json"):
263
245
  notes.append("缺少 recap_run_manifest.json:无法证明 work_dir 属于当前视频/参数(resume 会被拒绝)。")
264
- if mode == "cut" and _present(work_dir, "narration.json"):
265
- ledger, _ = _load_json(Path(work_dir) / "recap_phase.json")
266
- if isinstance(ledger, dict):
267
- recorded = ledger.get("clip_plan_fingerprint")
268
- if recorded is None:
269
- notes.append("recap_phase.json 未记录 clip_plan_fingerprint:无法判断 narration 是否对当前剪辑写的。")
270
- else:
271
- notes.append("有 narration.json 但缺少 recap_phase.json:无法判断解说是否对当前剪辑写的(可能 stale)。")
246
+ if mode == "cut" and _present(work_dir, "narration.json") and not _present(work_dir, "recap_phase.json"):
247
+ notes.append("有 narration.json 但缺少 recap_phase.json:无法判断解说是否对当前剪辑写的(可能 stale)。")
272
248
  return notes
273
249
 
274
250
 
@@ -277,44 +253,44 @@ def _present_storyboards(work_dir):
277
253
 
278
254
 
279
255
  def cmd_state(work_dir, compact=None):
280
- del compact # Retained for callers that used the pre-v0.4 rendering hint.
256
+ del compact
281
257
  work_dir = Path(work_dir)
282
258
  if not work_dir.exists():
283
259
  return {"error": f"work_dir 不存在: {work_dir}"}
284
260
 
285
- forward = [n for n in _FORWARD_STATE_FILES if _present(work_dir, n)]
261
+ forward = [name for name in _FORWARD_STATE_FILES if _present(work_dir, name)]
286
262
  mode = _detect_mode(work_dir)
287
- source = _discover_source(work_dir)
288
-
289
263
  groups = {
290
264
  "understanding": _UNDERSTANDING_ARTIFACTS,
291
265
  "cut": _CUT_ARTIFACTS,
292
266
  "script": _SCRIPT_ARTIFACTS,
293
267
  "render": _RENDER_ARTIFACTS,
294
268
  }
295
- artifacts = {}
296
- for group, names in groups.items():
297
- artifacts[group] = {
269
+ artifacts = {
270
+ group: {
298
271
  "present": [n for n in names if _present(work_dir, n)],
299
272
  "missing": [n for n in names if not _present(work_dir, n)],
300
273
  }
301
-
274
+ for group, names in groups.items()
275
+ }
302
276
  pause = _next_pause(work_dir, mode)
303
277
  return {
304
278
  "work_dir": str(work_dir),
305
279
  "mode": mode,
306
280
  "forward_state_files": forward,
307
- "source_video": source,
281
+ "source_video": _discover_source(work_dir),
308
282
  "multi_source": _discover_multi_source(work_dir),
309
- "next_pause": (
310
- {"artifact": pause[0], "hint": pause[1]} if pause else None
311
- ),
283
+ "next_pause": {"artifact": pause[0], "hint": pause[1]} if pause else None,
312
284
  "artifacts": artifacts,
313
285
  "storyboards": _present_storyboards(work_dir),
314
286
  "stale_manifest_notes": _stale_manifest_note(work_dir, mode),
315
287
  }
316
288
 
317
289
 
290
+ def _short_fingerprint(fp):
291
+ return f"{fp[:12]}…" if fp else "unknown"
292
+
293
+
318
294
  def _render_state_md(state, compact):
319
295
  if "error" in state:
320
296
  return state["error"]
@@ -323,18 +299,17 @@ def _render_state_md(state, compact):
323
299
  lines.append(f"状态来源(write-side manifest): {', '.join(state['forward_state_files'])}")
324
300
  lines.append(f"模式: **{state['mode']}**")
325
301
  src = state["source_video"]
326
- path = src["path"] or "unknown"
327
- fp = src["fingerprint"]
328
- fp_short = (fp[:12] + "…") if isinstance(fp, str) and len(fp) > 12 else (fp or "unknown")
329
- lines.append(f"源视频: {_truncate(path, compact)} (fp {fp_short}, 来源 {src['origin']})")
330
- if state.get("multi_source"):
302
+ lines.append(
303
+ f"源视频: {_truncate(src['path'] or 'unknown', compact)} "
304
+ f"(fp {_short_fingerprint(src['fingerprint'])}, 来源 {src['origin']})"
305
+ )
306
+ if state["multi_source"]:
331
307
  lines.append("多源素材:")
332
- for s in state["multi_source"].get("sources", []):
333
- fp = s.get("source_video_fingerprint")
334
- short = (fp[:12] + "…") if isinstance(fp, str) and len(fp) > 12 else (fp or "unknown")
308
+ for s in state["multi_source"]["sources"]:
335
309
  lines.append(
336
- f" - {s.get('source_id')}: {_truncate(s.get('source_path'), compact)} "
337
- f"(fp {short}, work_dir {s.get('source_work_dir')}, material {s.get('material_id')})"
310
+ f" - {s['source_id']}: {_truncate(s['source_path'], compact)} "
311
+ f"(fp {_short_fingerprint(s['source_video_fingerprint'])}, "
312
+ f"work_dir {s['source_work_dir']}, material {s['material_id']})"
338
313
  )
339
314
  lines.append("")
340
315
 
@@ -376,15 +351,16 @@ def cmd_clip_map(work_dir, output_start, output_end, source_start, source_end, c
376
351
  if not validated.exists():
377
352
  return {"error": "clip_plan_validated.json 不存在:这不是 cut 运行,或剪辑计划尚未被 cut.py 校验。"
378
353
  "(full 模式没有源↔输出映射;cut 模式请先跑 video-cut。)"}
379
- plan, err = _load_json(validated)
380
- if err:
381
- return {"error": err}
354
+ try:
355
+ plan = load_json(validated)
356
+ except (OSError, ValueError):
357
+ return {"error": "clip_plan_validated.json 不是合法 JSON(可能写坏了)"}
382
358
  entries = _clip_entries(plan)
383
359
  if entries is None:
384
- return {"error": "clip_plan_validated.json 结构异常:既不是 clips 数组也不是 {\"clips\": [...]}。"}
360
+ return {"error": "clip_plan_validated.json 结构异常:需要 clips 数组。"}
385
361
  clips = _normalize_clips(entries)
386
362
  if not clips:
387
- return {"error": "clip_plan_validated.json 没有有效的 clip(每段需要 source_start/source_end)。"}
363
+ return {"error": "clip_plan_validated.json 没有有效的 clip。"}
388
364
 
389
365
  have_output = output_start is not None or output_end is not None
390
366
  have_source = source_start is not None or source_end is not None
@@ -407,10 +383,22 @@ def cmd_clip_map(work_dir, output_start, output_end, source_start, source_end, c
407
383
  return result
408
384
 
409
385
 
386
+ def _segment(clip, *, source, output, compact):
387
+ # source_id/source_path are only written for multi-source cuts.
388
+ return {
389
+ "clip_id": clip["clip_id"],
390
+ "source_id": clip.get("source_id"),
391
+ "source_path": clip.get("source_path"),
392
+ "output": output,
393
+ "source": source,
394
+ "reason": _truncate(clip.get("reason"), compact),
395
+ }
396
+
397
+
410
398
  def _map_output_window(out_start, out_end, clips, compact):
411
399
  """Map a queried OUTPUT window to its SOURCE window(s), one per touched clip."""
412
- out_lo = clips[0]["output_start"] if out_start is None else float(out_start)
413
- out_hi = clips[-1]["output_end"] if out_end is None else float(out_end)
400
+ out_lo = clips[0]["output_start"] if out_start is None else out_start
401
+ out_hi = clips[-1]["output_end"] if out_end is None else out_end
414
402
  if out_hi < out_lo:
415
403
  out_lo, out_hi = out_hi, out_lo
416
404
  segments = []
@@ -418,14 +406,12 @@ def _map_output_window(out_start, out_end, clips, compact):
418
406
  ov = _overlap(out_lo, out_hi, clip["output_start"], clip["output_end"])
419
407
  if not ov:
420
408
  continue
421
- segments.append({
422
- "clip_id": clip["clip_id"],
423
- "source_id": clip.get("source_id"),
424
- "source_path": clip.get("source_path"),
425
- "output": [round(ov[0], 3), round(ov[1], 3)],
426
- "source": [_output_to_source(ov[0], clip), _output_to_source(ov[1], clip)],
427
- "reason": _truncate(clip.get("reason"), compact),
428
- })
409
+ segments.append(_segment(
410
+ clip,
411
+ output=[round(ov[0], 3), round(ov[1], 3)],
412
+ source=[_output_to_source(ov[0], clip), _output_to_source(ov[1], clip)],
413
+ compact=compact,
414
+ ))
429
415
  return {
430
416
  "direction": "output→source",
431
417
  "query": [round(out_lo, 3), round(out_hi, 3)],
@@ -439,8 +425,8 @@ def _map_output_window(out_start, out_end, clips, compact):
439
425
 
440
426
  def _map_source_window(src_start, src_end, clips, compact):
441
427
  """Map a queried SOURCE window to its OUTPUT window(s), flagging source ranges in no clip."""
442
- src_lo = clips[0]["source_start"] if src_start is None else float(src_start)
443
- src_hi = clips[-1]["source_end"] if src_end is None else float(src_end)
428
+ src_lo = clips[0]["source_start"] if src_start is None else src_start
429
+ src_hi = clips[-1]["source_end"] if src_end is None else src_end
444
430
  if src_hi < src_lo:
445
431
  src_lo, src_hi = src_hi, src_lo
446
432
  segments = []
@@ -450,14 +436,12 @@ def _map_source_window(src_start, src_end, clips, compact):
450
436
  if not ov:
451
437
  continue
452
438
  covered.append(ov)
453
- segments.append({
454
- "clip_id": clip["clip_id"],
455
- "source_id": clip.get("source_id"),
456
- "source_path": clip.get("source_path"),
457
- "source": [round(ov[0], 3), round(ov[1], 3)],
458
- "output": [_source_to_output(ov[0], clip), _source_to_output(ov[1], clip)],
459
- "reason": _truncate(clip.get("reason"), compact),
460
- })
439
+ segments.append(_segment(
440
+ clip,
441
+ source=[round(ov[0], 3), round(ov[1], 3)],
442
+ output=[_source_to_output(ov[0], clip), _source_to_output(ov[1], clip)],
443
+ compact=compact,
444
+ ))
461
445
  # Cut-out gaps: parts of [src_lo, src_hi] covered by NO kept clip (footage that was cut away).
462
446
  gaps = []
463
447
  cursor = src_lo
@@ -496,9 +480,9 @@ def _render_clip_map_md(result, compact):
496
480
  for s in q["segments"]:
497
481
  src = f"{_fmt_seconds(s['source'][0])}–{_fmt_seconds(s['source'][1])}"
498
482
  out = f"{_fmt_seconds(s['output'][0])}–{_fmt_seconds(s['output'][1])}"
499
- reason = f" 〔{s['reason']}〕" if s.get("reason") else ""
500
- sid = f" {s.get('source_id')}" if s.get("source_id") else ""
501
- spath = f" `{_truncate(s.get('source_path'), compact)}`" if s.get("source_path") else ""
483
+ reason = f" 〔{s['reason']}〕" if s["reason"] else ""
484
+ sid = f" {s['source_id']}" if s["source_id"] else ""
485
+ spath = f" `{_truncate(s['source_path'], compact)}`" if s["source_path"] else ""
502
486
  lines.append(f" clip {s['clip_id']}{sid}{spath}: 源 {src} ↔ 输出 {out}{reason}")
503
487
  if q["cut_out_source_gaps"]:
504
488
  lines.append(" ✂️ 被剪掉的源区间(不在成片里):")
@@ -1,35 +1,45 @@
1
1
  """Apply the optional or strict narration-review gate before TTS."""
2
2
 
3
+ import json
4
+ import os
3
5
  from pathlib import Path
4
6
 
5
- from recap_runtime import _env_bool, _load_json
6
-
7
7
 
8
8
  def review_narration_enabled(args):
9
- if getattr(args, "review_narration", None) is not None:
10
- return bool(args.review_narration)
9
+ if args.review_narration is not None:
10
+ return args.review_narration
11
11
  return _env_bool("REVIEW_NARRATION", True)
12
12
 
13
13
 
14
14
  def require_narration_review(args):
15
- if getattr(args, "require_narration_review", False):
16
- return True
17
- return _env_bool("REQUIRE_NARRATION_REVIEW", False)
15
+ return args.require_narration_review or _env_bool("REQUIRE_NARRATION_REVIEW", False)
16
+
17
+
18
+ def _env_bool(name, default):
19
+ raw = os.environ.get(name)
20
+ return default if not raw else raw.strip().lower() in {"1", "true", "yes", "y", "on"}
18
21
 
19
22
 
20
23
  def review_result_status(work_dir):
21
- data = _load_json(Path(work_dir) / "narration_review.json")
24
+ path = Path(work_dir) / "narration_review.json"
25
+ if not path.exists():
26
+ return {"ok": False, "reason": "missing or invalid narration_review.json"}
27
+ try:
28
+ data = json.loads(path.read_text(encoding="utf-8"))
29
+ except (OSError, json.JSONDecodeError):
30
+ return {"ok": False, "reason": "missing or invalid narration_review.json"}
22
31
  if not isinstance(data, dict):
23
32
  return {"ok": False, "reason": "missing or invalid narration_review.json"}
24
- findings = [f for f in (data.get("findings") or []) if isinstance(f, dict)]
25
- error_count = sum(1 for finding in findings if finding.get("severity") == "error")
33
+ findings = data.get("findings")
34
+ if not isinstance(findings, list) or any(
35
+ not isinstance(finding, dict)
36
+ or not isinstance(finding.get("severity"), str)
37
+ for finding in findings
38
+ ):
39
+ return {"ok": False, "reason": "missing or invalid narration_review.json"}
40
+ error_count = sum(1 for finding in findings if finding["severity"] == "error")
26
41
  if data.get("parse_error"):
27
- return {
28
- "ok": False,
29
- "reason": "parse_error",
30
- "review": data,
31
- "errors": error_count,
32
- }
42
+ return {"ok": False, "reason": "parse_error", "review": data, "errors": error_count}
33
43
  if error_count:
34
44
  return {
35
45
  "ok": False,
@@ -45,10 +55,7 @@ def review_result_status(work_dir):
45
55
  def clear_narration_review_artifacts(work_dir):
46
56
  """Remove prior review artifacts before a fresh pre-TTS review run."""
47
57
  for name in ("narration_review.json", "narration_review.md"):
48
- try:
49
- (Path(work_dir) / name).unlink()
50
- except FileNotFoundError:
51
- pass
58
+ (Path(work_dir) / name).unlink(missing_ok=True)
52
59
 
53
60
 
54
61
  def run_narration_review(work_dir, args, *, run, timeline="source"):