@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
@@ -2,6 +2,7 @@
2
2
  Merged from the shared core; reads the same env vars as the rest of the bundle."""
3
3
  import json
4
4
  import hashlib
5
+ import math
5
6
  import os
6
7
  import re
7
8
  import subprocess
@@ -40,34 +41,48 @@ def is_mimo_token_plan_key(api_key):
40
41
  return str(api_key or "").strip().startswith("tp-")
41
42
 
42
43
 
43
- def default_mimo_api_url(api_key="", cluster=None):
44
+ def default_mimo_api_url(is_token_plan, cluster=None):
44
45
  """Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
45
46
 
46
47
  MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
47
48
  (`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
48
49
  not the pay-as-you-go `api.xiaomimimo.com` endpoint.
50
+
51
+ The caller classifies its own key with `is_mimo_token_plan_key` and passes only
52
+ that bit: a credential never reaches a function whose return value is logged.
49
53
  """
50
- if is_mimo_token_plan_key(api_key):
54
+ if is_token_plan:
51
55
  cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
52
56
  cluster_name = str(cluster_name).strip().lower()
53
- return MIMO_TOKEN_PLAN_API_URLS.get(cluster_name, MIMO_TOKEN_PLAN_API_URLS[DEFAULT_MIMO_TOKEN_PLAN_CLUSTER])
57
+ if cluster_name not in MIMO_TOKEN_PLAN_API_URLS:
58
+ raise ValueError(
59
+ f"MiMo token-plan cluster must be one of {sorted(MIMO_TOKEN_PLAN_API_URLS)}; "
60
+ f"got {cluster_name!r}"
61
+ )
62
+ return MIMO_TOKEN_PLAN_API_URLS[cluster_name]
54
63
  return DEFAULT_MIMO_API_URL
55
64
 
56
65
 
57
- def env_int(name, default, *, minimum=None):
58
- """Read an integer env var; ignore malformed values instead of crashing import."""
66
+ def _env_number(name, default, cast, minimum):
59
67
  raw = os.environ.get(name)
60
68
  if raw is None or raw == "":
61
69
  return default
62
70
  try:
63
- value = int(raw)
64
- except (TypeError, ValueError):
65
- return default
66
- if minimum is not None:
67
- value = max(minimum, value)
71
+ value = cast(raw)
72
+ except ValueError as exc:
73
+ raise ValueError(f"{name} must be a {cast.__name__}; got {raw!r}") from exc
74
+ if isinstance(value, float) and not math.isfinite(value):
75
+ raise ValueError(f"{name} must be finite; got {raw!r}")
76
+ if minimum is not None and value < minimum:
77
+ raise ValueError(f"{name} must be >= {minimum}; got {value}")
68
78
  return value
69
79
 
70
80
 
81
+ def env_int(name, default, *, minimum=None):
82
+ """Read an integer env var, rejecting malformed or below-minimum values."""
83
+ return _env_number(name, default, int, minimum)
84
+
85
+
71
86
  def env_bool(name, default=False):
72
87
  """Read common boolean env var forms."""
73
88
  raw = os.environ.get(name)
@@ -77,17 +92,8 @@ def env_bool(name, default=False):
77
92
 
78
93
 
79
94
  def env_float(name, default, *, minimum=None):
80
- """Read a float env var; ignore malformed values instead of crashing import."""
81
- raw = os.environ.get(name)
82
- if raw is None or raw == "":
83
- return default
84
- try:
85
- value = float(raw)
86
- except (TypeError, ValueError):
87
- return default
88
- if minimum is not None:
89
- value = max(minimum, value)
90
- return value
95
+ """Read a float env var, rejecting malformed or below-minimum values."""
96
+ return _env_number(name, default, float, minimum)
91
97
 
92
98
 
93
99
  # Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
@@ -97,16 +103,16 @@ def env_float(name, default, *, minimum=None):
97
103
  _mimo_api_key = os.environ.get("MIMO_API_KEY", "")
98
104
  _mimo_video_api_key = os.environ.get("MIMO_VIDEO_API_KEY", "") or _mimo_api_key
99
105
  _mimo_asr_api_key = os.environ.get("MIMO_ASR_API_KEY", "") or _mimo_api_key
100
- _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
106
+ _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(is_mimo_token_plan_key(_mimo_api_key))
101
107
  _raw_mimo_video_api_url = (
102
108
  os.environ.get("MIMO_VIDEO_API_URL")
103
109
  or os.environ.get("MIMO_API_URL")
104
- or default_mimo_api_url(_mimo_video_api_key)
110
+ or default_mimo_api_url(is_mimo_token_plan_key(_mimo_video_api_key))
105
111
  )
106
112
  _raw_mimo_asr_api_url = (
107
113
  os.environ.get("MIMO_ASR_API_URL")
108
114
  or os.environ.get("MIMO_API_URL")
109
- or default_mimo_api_url(_mimo_asr_api_key)
115
+ or default_mimo_api_url(is_mimo_token_plan_key(_mimo_asr_api_key))
110
116
  )
111
117
 
112
118
  CONFIG = {
@@ -410,8 +416,7 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
410
416
  try:
411
417
  req = urllib.request.Request(endpoint, data=data, headers=headers)
412
418
  with urllib.request.urlopen(req, timeout=300) as resp:
413
- result = json.loads(resp.read().decode("utf-8"))
414
- return result
419
+ return json.loads(resp.read().decode("utf-8"))
415
420
  except urllib.error.HTTPError as e:
416
421
  body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
417
422
  wait = min(2 ** attempt, 60)
@@ -1,11 +1,13 @@
1
- """Enforce narration timing, evidence, budget, and sentence-integrity rules."""
1
+ """Enforce narration timing, evidence, budget, and sentence-integrity rules.
2
+
3
+ This is the single validator for the agent-authored narration.json: shape, numeric
4
+ timing and text checks live here, and every later consumer trusts its result.
5
+ """
2
6
 
3
- import importlib.util
4
7
  import json
5
8
  from pathlib import Path
6
9
 
7
- from lib import CONFIG
8
- from lib import log
10
+ from lib import CONFIG, log
9
11
  from agent_text import (
10
12
  _clean_narration_punctuation,
11
13
  _find_scene_for_midpoint,
@@ -16,24 +18,12 @@ from agent_text import (
16
18
  _text_char_count,
17
19
  _truncate_at_sentence,
18
20
  )
21
+ from deslop_qc import analyze_deslop_qc
19
22
  from speech_ownership import (
20
23
  entry_overlaps_source_speech,
21
24
  load_source_sentence_evidence,
22
25
  )
23
26
 
24
- try:
25
- from deslop_qc import analyze_deslop_qc
26
- except ModuleNotFoundError:
27
- _deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
28
- _deslop_qc_spec = importlib.util.spec_from_file_location(
29
- "deslop_qc", _deslop_qc_path
30
- )
31
- if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
32
- raise
33
- _deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
34
- _deslop_qc_spec.loader.exec_module(_deslop_qc_module)
35
- analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
36
-
37
27
 
38
28
  def _lint_issue(level, index, code, message, **extra):
39
29
  issue = {"level": level, "index": index, "code": code, "message": message}
@@ -41,11 +31,64 @@ def _lint_issue(level, index, code, message, **extra):
41
31
  return issue
42
32
 
43
33
 
34
+ def _is_number(value):
35
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
36
+
37
+
38
+ def _visual_overlay_issues(index, seg):
39
+ """Shape-check the renderer handoff recap reads verbatim from a validated segment.
40
+
41
+ recap owns which overlay `type` values it actually renders and drops the rest; lint owns
42
+ the shape, because nothing between here and that filter re-checks it — and the filter runs
43
+ after TTS, so a malformed overlay caught there would already have cost a synthesis pass.
44
+ """
45
+ overlays = seg.get("visual_overlays")
46
+ if overlays is None:
47
+ return []
48
+ if not isinstance(overlays, list):
49
+ return [
50
+ _lint_issue(
51
+ "error", index, "invalid_visual_overlays", "visual_overlays must be an array"
52
+ )
53
+ ]
54
+ issues = []
55
+ for position, overlay in enumerate(overlays):
56
+ if not isinstance(overlay, dict):
57
+ issues.append(
58
+ _lint_issue(
59
+ "error", index, "invalid_visual_overlay",
60
+ "visual_overlays entries must be objects",
61
+ overlay_index=position,
62
+ )
63
+ )
64
+ continue
65
+ for key in ("type", "text"):
66
+ value = overlay.get(key)
67
+ if not isinstance(value, str) or not value.strip():
68
+ issues.append(
69
+ _lint_issue(
70
+ "error", index, "invalid_visual_overlay",
71
+ f"visual_overlays[].{key} must be a non-empty string",
72
+ overlay_index=position,
73
+ )
74
+ )
75
+ for key in ("start", "end"):
76
+ if key in overlay and not _is_number(overlay[key]):
77
+ issues.append(
78
+ _lint_issue(
79
+ "error", index, "invalid_visual_overlay",
80
+ f"visual_overlays[].{key} must be numeric",
81
+ overlay_index=position,
82
+ )
83
+ )
84
+ return issues
85
+
86
+
44
87
  def _scene_bounds_for_midpoint(scenes_analysis, start, end):
45
- scene = _find_scene_for_midpoint(scenes_analysis or [], start, end)
88
+ scene = _find_scene_for_midpoint(scenes_analysis, start, end)
46
89
  if not scene:
47
90
  return None
48
- return float(scene.get("start", 0)), float(scene.get("end", 0))
91
+ return scene["start"], scene["end"]
49
92
 
50
93
 
51
94
  def _frame_fact_times_for_segment(scenes_analysis, start, end):
@@ -55,53 +98,38 @@ def _frame_fact_times_for_segment(scenes_analysis, start, end):
55
98
  many anchors is more likely to drift into "general story summary" instead
56
99
  of staying attached to what is on screen.
57
100
  """
58
- times = []
59
- scene = _find_scene_for_midpoint(scenes_analysis or [], start, end)
101
+ scene = _find_scene_for_midpoint(scenes_analysis, start, end)
60
102
  if not scene:
61
- return times
62
- for raw_ts in (scene.get("frame_facts") or {}).keys():
63
- try:
64
- ts = float(raw_ts)
65
- except (TypeError, ValueError):
66
- continue
67
- if float(start) <= ts <= float(end):
68
- times.append(ts)
69
- return sorted(times)
103
+ return []
104
+ return sorted(
105
+ ts
106
+ for ts in map(float, scene.get("frame_facts", {}))
107
+ if start <= ts <= end
108
+ )
109
+
110
+
111
+ def _clip_span(clip):
112
+ """A validated plan clip carries source_start/source_end; a raw agent plan start/end."""
113
+ if "source_start" in clip:
114
+ return clip["source_start"], clip["source_end"]
115
+ return clip["start"], clip["end"]
70
116
 
71
117
 
72
118
  def _clip_matches_for_segment(seg, clip_plan):
73
119
  if not clip_plan:
74
120
  return []
75
- clips = (
76
- clip_plan.get("clips", clip_plan) if isinstance(clip_plan, dict) else clip_plan
77
- )
78
- if not isinstance(clips, list):
79
- return []
80
- try:
81
- start = float(seg.get("start"))
82
- end = float(seg.get("end"))
83
- except (TypeError, ValueError):
84
- return []
85
- midpoint = (start + end) / 2
121
+ clips = clip_plan["clips"]
122
+ midpoint = (seg["start"] + seg["end"]) / 2
86
123
  if seg.get("source_clip_id") is not None:
87
124
  try:
88
- requested = int(seg.get("source_clip_id"))
125
+ requested = int(seg["source_clip_id"])
89
126
  except (TypeError, ValueError):
90
127
  return []
91
- return [
92
- clip
93
- for clip in clips
94
- if clip.get("clip_id") == requested
95
- and float(clip.get("source_start", clip.get("start", 0)))
96
- <= midpoint
97
- <= float(clip.get("source_end", clip.get("end", 0)))
98
- ]
128
+ clips = [clip for clip in clips if clip.get("clip_id") == requested]
99
129
  return [
100
130
  clip
101
131
  for clip in clips
102
- if float(clip.get("source_start", clip.get("start", 0)))
103
- <= midpoint
104
- <= float(clip.get("source_end", clip.get("end", 0)))
132
+ if _clip_span(clip)[0] <= midpoint <= _clip_span(clip)[1]
105
133
  ]
106
134
 
107
135
 
@@ -132,17 +160,13 @@ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
132
160
  # already be several Chinese syllables into the next sentence.
133
161
  for anchor in anchors:
134
162
  when = anchor["time"]
135
- try:
136
- pause_start = float(anchor.get("pause_start", when - 0.12))
137
- except (TypeError, ValueError):
138
- pause_start = when - 0.12
163
+ pause_start = anchor.get("pause_start", when - 0.12)
139
164
  if pause_start - 0.05 <= start <= when + 0.08:
140
165
  return None
141
166
 
142
167
  suggested = next(
143
168
  (anchor for anchor in anchors if anchor["time"] > start + 0.08), None
144
169
  )
145
- text_tail = str((suggested or {}).get("text_tail") or "").strip()
146
170
  return _lint_issue(
147
171
  "error",
148
172
  index,
@@ -151,16 +175,31 @@ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
151
175
  "sentence-end anchor, or shorten/move/remove it when there is no later verified anchor, "
152
176
  "then rerun lint before TTS. Source sentence interruption has no override.",
153
177
  entry_time=round(start, 3),
154
- suggested_start=round(float(suggested["time"]), 3) if suggested else None,
155
- source_text_tail=text_tail,
156
- anchor_confidence=suggested.get("confidence") if suggested else None,
178
+ suggested_start=round(suggested["time"], 3) if suggested else None,
179
+ source_text_tail=str(suggested.get("text_tail", "")).strip() if suggested else "",
180
+ anchor_confidence=suggested["confidence"] if suggested else None,
157
181
  )
158
182
 
159
183
 
184
+ def _has_connected_predecessor(narration, idx, start):
185
+ """A back-to-back narration handoff (<=150ms gap) does not expose the source track,
186
+ so the next TTS block is not a new source-speech entry."""
187
+ for other_idx, other in enumerate(narration):
188
+ if other_idx == idx or not isinstance(other, dict):
189
+ continue
190
+ other_start, other_end = other.get("start"), other.get("end")
191
+ if not _is_number(other_start) or not _is_number(other_end):
192
+ continue
193
+ if other_start < start and -0.001 <= start - other_end <= 0.15:
194
+ return True
195
+ return False
196
+
197
+
160
198
  def lint_narration(
161
199
  narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
162
200
  ):
163
201
  """Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set."""
202
+ scenes_analysis = scenes_analysis or []
164
203
  errors = []
165
204
  warnings = []
166
205
  normalized = []
@@ -187,10 +226,8 @@ def lint_narration(
187
226
  )
188
227
  )
189
228
  continue
190
- try:
191
- start = float(seg.get("start"))
192
- end = float(seg.get("end"))
193
- except (TypeError, ValueError):
229
+ start, end = seg.get("start"), seg.get("end")
230
+ if not _is_number(start) or not _is_number(end):
194
231
  errors.append(
195
232
  _lint_issue(
196
233
  "error", idx, "invalid_time", "start/end must be numeric"
@@ -198,19 +235,18 @@ def lint_narration(
198
235
  )
199
236
  continue
200
237
  text = str(seg.get("narration", "")).strip()
201
- pause_raw = seg.get("pause_after_ms", CONFIG.get("breath_ms", 250))
202
238
  try:
203
- pause = int(pause_raw)
239
+ pause = int(seg.get("pause_after_ms", CONFIG["breath_ms"]))
204
240
  except (TypeError, ValueError):
205
- pause = CONFIG.get("breath_ms", 250)
206
- warnings.append(
241
+ errors.append(
207
242
  _lint_issue(
208
- "warning",
243
+ "error",
209
244
  idx,
210
245
  "invalid_pause",
211
- "pause_after_ms is invalid; default will be used",
246
+ "pause_after_ms must be an integer number of milliseconds",
212
247
  )
213
248
  )
249
+ continue
214
250
  if pause < 0:
215
251
  warnings.append(
216
252
  _lint_issue(
@@ -221,6 +257,7 @@ def lint_narration(
221
257
  pause_after_ms=pause,
222
258
  )
223
259
  )
260
+ errors.extend(_visual_overlay_issues(idx, seg))
224
261
  if end <= start:
225
262
  errors.append(
226
263
  _lint_issue(
@@ -247,16 +284,12 @@ def lint_narration(
247
284
  continue
248
285
 
249
286
  char_count = _text_char_count(text)
250
- budget = _recommended_char_budget(start, end, pause)
287
+ budget = _recommended_char_budget(start, end)
251
288
  # estimate at the REAL playback rate (after the narration_speed atempo); otherwise a
252
289
  # beat sized to its 1.3x-sped slot looks "over budget" when it actually fits.
253
- play_rate = max(
254
- CONFIG.get("speech_rate", 3.5)
255
- * float(CONFIG.get("narration_speed", 1.0) or 1.0),
256
- 0.1,
257
- )
290
+ play_rate = CONFIG["speech_rate"] * CONFIG["narration_speed"]
258
291
  estimated_tts_seconds = char_count / play_rate
259
- slot_seconds = _scene_available_seconds(start, end, pause)
292
+ slot_seconds = _scene_available_seconds(start, end)
260
293
  if budget < 5:
261
294
  warnings.append(
262
295
  _lint_issue(
@@ -295,27 +328,16 @@ def lint_narration(
295
328
  )
296
329
  )
297
330
 
298
- # A back-to-back narration handoff does not expose the source track, so
299
- # the next TTS block is not a new source-speech entry. Keep this strict:
300
- # only authored adjacency (<=150ms) bypasses the sentence anchor gate.
301
- connected_predecessor = False
302
- for other_idx, other in enumerate(narration):
303
- if other_idx == idx or not isinstance(other, dict):
304
- continue
305
- try:
306
- other_start = float(other.get("start"))
307
- other_end = float(other.get("end"))
308
- except (TypeError, ValueError):
309
- continue
310
- if other_start < start and -0.001 <= start - other_end <= 0.15:
311
- connected_predecessor = True
312
- break
313
- if not connected_predecessor:
331
+ if not _has_connected_predecessor(narration, idx, start):
314
332
  entry_issue = _source_sentence_entry_issue(
315
333
  idx,
316
334
  start,
317
335
  source_sentence_anchors,
318
- entry_overlaps_source_speech(seg, source_evidence),
336
+ entry_overlaps_source_speech(
337
+ start,
338
+ source_evidence,
339
+ authored_overlap=seg.get("overlaps_speech", True),
340
+ ),
319
341
  )
320
342
  if entry_issue:
321
343
  errors.append(entry_issue)
@@ -349,13 +371,9 @@ def lint_narration(
349
371
  frame_fact_times = _frame_fact_times_for_segment(
350
372
  scenes_analysis, start, end
351
373
  )
352
- max_visual_seconds = float(
353
- CONFIG.get("visual_beat_max_seconds", 18.0) or 18.0
354
- )
355
- max_visual_facts = int(CONFIG.get("visual_beat_max_facts", 3) or 3)
356
- if (end - start) > max_visual_seconds and len(
374
+ if (end - start) > CONFIG["visual_beat_max_seconds"] and len(
357
375
  frame_fact_times
358
- ) > max_visual_facts:
376
+ ) > CONFIG["visual_beat_max_facts"]:
359
377
  warnings.append(
360
378
  _lint_issue(
361
379
  "warning",
@@ -394,11 +412,7 @@ def lint_narration(
394
412
  )
395
413
  )
396
414
  if len(matches) == 1:
397
- clip = matches[0]
398
- clip_start = float(
399
- clip.get("source_start", clip.get("start", start))
400
- )
401
- clip_end = float(clip.get("source_end", clip.get("end", end)))
415
+ clip_start, clip_end = _clip_span(matches[0])
402
416
  if start < clip_start or end > clip_end:
403
417
  warnings.append(
404
418
  _lint_issue(
@@ -414,7 +428,7 @@ def lint_narration(
414
428
  )
415
429
  if seg.get("source_clip_id") is not None:
416
430
  try:
417
- int(seg.get("source_clip_id"))
431
+ int(seg["source_clip_id"])
418
432
  except (TypeError, ValueError):
419
433
  errors.append(
420
434
  _lint_issue(
@@ -462,23 +476,15 @@ def lint_narration(
462
476
  if mode == "full" and len(sorted_segments) >= 2:
463
477
  timeline_start = 0.0
464
478
  timeline_end = sorted_segments[-1]["end"]
465
- scene_ends = []
466
- for scene in scenes_analysis or []:
467
- try:
468
- scene_ends.append(float(scene.get("end")))
469
- except (AttributeError, TypeError, ValueError):
470
- continue
479
+ scene_ends = [scene["end"] for scene in scenes_analysis]
471
480
  if scene_ends:
472
481
  timeline_end = max(timeline_end, max(scene_ends))
473
482
  span = max(0.0, timeline_end - timeline_start)
474
483
  # Score coverage at the SAME conservative rate the writer is budgeted at
475
484
  # (_recommended_char_budget uses speech_rate * speech_safety_margin * narration_speed),
476
485
  # else the metric scores ~18% stricter than its own budget and false-flags under_narrated.
477
- play_rate = max(
478
- CONFIG["speech_rate"]
479
- * float(CONFIG.get("speech_safety_margin", 0.85) or 0.85)
480
- * float(CONFIG.get("narration_speed", 1.0) or 1.0),
481
- 0.1,
486
+ play_rate = (
487
+ CONFIG["speech_rate"] * CONFIG["speech_safety_margin"] * CONFIG["narration_speed"]
482
488
  )
483
489
  spoken = [s["char_count"] / play_rate for s in sorted_segments]
484
490
  spoken_ends = [
@@ -501,7 +507,7 @@ def lint_narration(
501
507
  merged_intervals.append([start, end])
502
508
  narrated_seconds = sum(end - start for start, end in merged_intervals)
503
509
  coverage = narrated_seconds / span if span > 0 else 0.0
504
- orig_min = CONFIG.get("original_block_min_seconds", 2.5)
510
+ orig_min = CONFIG["original_block_min_seconds"]
505
511
  orig_gaps = [sorted_segments[0]["start"] - timeline_start]
506
512
  orig_gaps.extend(
507
513
  sorted_segments[i + 1]["start"] - spoken_ends[i]
@@ -510,10 +516,10 @@ def lint_narration(
510
516
  orig_gaps.append(timeline_end - spoken_ends[-1])
511
517
  original_blocks = sum(1 for g in orig_gaps if g >= orig_min)
512
518
  avg_chars = sum(s["char_count"] for s in sorted_segments) / len(sorted_segments)
513
- cov_target = CONFIG.get("narration_coverage_target", 0.7)
514
- cov_max = CONFIG.get("narration_coverage_max", 0.85)
515
- cov_min = CONFIG.get("narration_coverage_min", 0.5)
516
- block_min_chars = CONFIG.get("narration_block_min_chars", 16)
519
+ cov_target = CONFIG["narration_coverage_target"]
520
+ cov_max = CONFIG["narration_coverage_max"]
521
+ cov_min = CONFIG["narration_coverage_min"]
522
+ block_min_chars = CONFIG["narration_block_min_chars"]
517
523
  metrics = {
518
524
  "segment_count": len(sorted_segments),
519
525
  "timeline_span_seconds": round(span, 2),
@@ -575,15 +581,20 @@ def lint_narration(
575
581
  )
576
582
  )
577
583
 
578
- deslop_qc = analyze_deslop_qc(narration, work_dir=work_dir)
579
- for blocker in deslop_qc.get("blockers", []):
584
+ deslop_qc = analyze_deslop_qc(
585
+ [seg for seg in narration if isinstance(seg, dict)]
586
+ if isinstance(narration, list)
587
+ else [],
588
+ work_dir=work_dir,
589
+ )
590
+ for blocker in deslop_qc["blockers"]:
580
591
  errors.append(
581
592
  _lint_issue(
582
593
  "error",
583
- blocker.get("index"),
584
- blocker.get("code", "deslop_qc_blocker"),
585
- blocker.get("message", "deslop QC objective blocker"),
586
- source=blocker.get("source"),
594
+ blocker["index"],
595
+ blocker["code"],
596
+ blocker["message"],
597
+ source=blocker["source"],
587
598
  matches=blocker.get("matches"),
588
599
  sentence=blocker.get("sentence"),
589
600
  )
@@ -616,7 +627,7 @@ def validate_narration_or_raise(
616
627
  )
617
628
  if report["errors"]:
618
629
  sample = "; ".join(
619
- f"#{e.get('index')}: {e['code']}" for e in report["errors"][:3]
630
+ f"#{e['index']}: {e['code']}" for e in report["errors"][:3]
620
631
  )
621
632
  raise ValueError(f"narration.json 预检失败: {sample}; 详见 narration_lint.json")
622
633
  if report["warnings"]:
@@ -629,18 +640,12 @@ def validate_narration_or_raise(
629
640
 
630
641
 
631
642
  def _validate_narration_budget(narration, scenes_analysis):
632
- """Validate agent-written narration against timing budgets; trim impossible text safely."""
633
- if not isinstance(narration, list):
634
- raise ValueError("narration.json 必须是 JSON 数组")
635
-
643
+ """Trim lint-validated narration to its timing budgets; drop what cannot be spoken."""
644
+ del scenes_analysis # scene boundaries are advisory (lint warns); authored timing is kept
636
645
  cleaned = []
637
646
  for raw in narration:
638
- item = _normalise_narration_segment(raw, scenes_analysis)
639
- if not item:
640
- continue
641
- max_chars = _recommended_char_budget(
642
- item["start"], item["end"], item.get("pause_after_ms")
643
- )
647
+ item = _normalise_narration_segment(raw)
648
+ max_chars = _recommended_char_budget(item["start"], item["end"])
644
649
  if max_chars < 5:
645
650
  log(f" 丢弃过短解说段 {item['start']:.1f}-{item['end']:.1f}s")
646
651
  continue