@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
@@ -13,8 +14,6 @@ from email.utils import parsedate_to_datetime
13
14
 
14
15
 
15
16
  # ── 配置 ──────────────────────────────────────────────────────────────
16
- _EXISTING_CONFIG_REF = globals().get("CONFIG")
17
-
18
17
  DEFAULT_MIMO_API_URL = "https://api.xiaomimimo.com/v1"
19
18
  DEFAULT_MIMO_TOKEN_PLAN_CLUSTER = "cn"
20
19
  MIMO_TOKEN_PLAN_API_URLS = {
@@ -22,7 +21,6 @@ MIMO_TOKEN_PLAN_API_URLS = {
22
21
  "sgp": "https://token-plan-sgp.xiaomimimo.com/v1",
23
22
  "ams": "https://token-plan-ams.xiaomimimo.com/v1",
24
23
  }
25
- DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
26
24
  DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
27
25
  DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
28
26
  DEFAULT_FISH_TTS_API_URL = "https://api.fish.audio/v1/tts"
@@ -32,7 +30,7 @@ DEFAULT_FISH_TTS_REFERENCE_ID = "5653cea4ac83480aaf2bf45406556185"
32
30
 
33
31
  def normalize_api_url(raw_url):
34
32
  """Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
35
- url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
33
+ url = raw_url.rstrip("/")
36
34
  if url.endswith("/chat/completions"):
37
35
  return url
38
36
  return f"{url}/chat/completions"
@@ -40,76 +38,80 @@ def normalize_api_url(raw_url):
40
38
 
41
39
  def is_mimo_token_plan_key(api_key):
42
40
  """Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
43
- return str(api_key or "").strip().startswith("tp-")
41
+ return api_key.startswith("tp-")
44
42
 
45
43
 
46
- def default_mimo_api_url(api_key="", cluster=None):
44
+ def default_mimo_api_url(is_token_plan):
47
45
  """Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
48
46
 
49
47
  MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
50
48
  (`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
51
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.
52
53
  """
53
- if is_mimo_token_plan_key(api_key):
54
- cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
55
- cluster_name = str(cluster_name).strip().lower()
56
- return MIMO_TOKEN_PLAN_API_URLS.get(cluster_name, MIMO_TOKEN_PLAN_API_URLS[DEFAULT_MIMO_TOKEN_PLAN_CLUSTER])
57
- return DEFAULT_MIMO_API_URL
54
+ if not is_token_plan:
55
+ return DEFAULT_MIMO_API_URL
56
+ cluster = os.environ.get("MIMO_TOKEN_PLAN_CLUSTER", DEFAULT_MIMO_TOKEN_PLAN_CLUSTER).strip().lower()
57
+ if cluster 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)}, got {cluster!r}"
60
+ )
61
+ return MIMO_TOKEN_PLAN_API_URLS[cluster]
58
62
 
59
63
 
60
- def env_int(name, default, *, minimum=None):
61
- """Read an integer env var; ignore malformed values instead of crashing import."""
64
+ def _env_number(name, default, cast, minimum):
62
65
  raw = os.environ.get(name)
63
66
  if raw is None or raw == "":
64
67
  return default
65
68
  try:
66
- value = int(raw)
67
- except (TypeError, ValueError):
68
- return default
69
- if minimum is not None:
70
- value = max(minimum, value)
69
+ value = cast(raw)
70
+ except ValueError as exc:
71
+ raise ValueError(f"{name} must be a {cast.__name__}, got {raw!r}") from exc
72
+ if isinstance(value, float) and not math.isfinite(value):
73
+ raise ValueError(f"{name} must be finite, got {raw!r}")
74
+ if minimum is not None and value < minimum:
75
+ raise ValueError(f"{name} must be >= {minimum}, got {raw!r}")
71
76
  return value
72
77
 
73
78
 
74
- def env_bool(name, default=False):
75
- """Read common boolean env var forms."""
76
- raw = os.environ.get(name)
77
- if raw is None or raw == "":
78
- return default
79
- return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
79
+ def env_int(name, default, *, minimum=None):
80
+ """Read an integer env var, rejecting malformed or below-minimum values."""
81
+ return _env_number(name, default, int, minimum)
80
82
 
81
83
 
82
84
  def env_float(name, default, *, minimum=None):
83
- """Read a float env var; ignore malformed values instead of crashing import."""
85
+ """Read a float env var, rejecting malformed or below-minimum values."""
86
+ return _env_number(name, default, float, minimum)
87
+
88
+
89
+ def env_bool(name, default=False):
90
+ """Read common boolean env var forms (1/true/yes/y/on, 0/false/no/n/off)."""
84
91
  raw = os.environ.get(name)
85
92
  if raw is None or raw == "":
86
93
  return default
87
- try:
88
- value = float(raw)
89
- except (TypeError, ValueError):
90
- return default
91
- if minimum is not None:
92
- value = max(minimum, value)
93
- return value
94
+ text = raw.strip().lower()
95
+ if text in {"1", "true", "yes", "y", "on"}:
96
+ return True
97
+ if text in {"0", "false", "no", "n", "off"}:
98
+ return False
99
+ raise ValueError(f"{name} must be a boolean (1/true/yes/on or 0/false/no/off), got {raw!r}")
94
100
 
95
101
 
96
- # Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
97
- # (MIMO_VIDEO_API_KEY / MIMO_TTS_API_KEY / MIMO_ASR_API_KEY and their *_API_URL forms)
98
- # are optional and fall back to MIMO_API_KEY / MIMO_API_URL. Token-Plan keys (tp-*) auto-
99
- # route to the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
102
+ # Single MiMo credential powers ASR + TTS. The TTS override (MIMO_TTS_API_KEY / MIMO_TTS_API_URL)
103
+ # is optional and falls back to MIMO_API_KEY / MIMO_API_URL. Token-Plan keys (tp-*) auto-route to
104
+ # the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
100
105
  _mimo_api_key = os.environ.get("MIMO_API_KEY", "")
101
106
  _mimo_tts_api_key = os.environ.get("MIMO_TTS_API_KEY", "") or _mimo_api_key
102
- _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
107
+ _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(is_mimo_token_plan_key(_mimo_api_key))
103
108
  _raw_mimo_tts_api_url = (
104
109
  os.environ.get("MIMO_TTS_API_URL")
105
110
  or os.environ.get("MIMO_API_URL")
106
- or default_mimo_api_url(_mimo_tts_api_key)
111
+ or default_mimo_api_url(is_mimo_token_plan_key(_mimo_tts_api_key))
107
112
  )
108
113
 
109
114
  CONFIG = {
110
- "api_url": normalize_api_url(_raw_api_url),
111
- "api_key": _mimo_api_key,
112
- "api_key_source": "MIMO_API_KEY",
113
115
  "mimo_api_url": normalize_api_url(_raw_api_url),
114
116
  "mimo_api_key": _mimo_api_key,
115
117
  "mimo_tts_api_url": normalize_api_url(_raw_mimo_tts_api_url),
@@ -145,19 +147,15 @@ CONFIG = {
145
147
  "tts_segment_target_rms_dbfs": env_float("TTS_SEGMENT_TARGET_RMS_DBFS", -20.0),
146
148
  "tts_segment_peak_limit": env_float("TTS_SEGMENT_PEAK_LIMIT", 0.98, minimum=0.1),
147
149
  }
148
- if isinstance(_EXISTING_CONFIG_REF, dict):
149
- _EXISTING_CONFIG_REF.clear()
150
- _EXISTING_CONFIG_REF.update(CONFIG)
151
- CONFIG = _EXISTING_CONFIG_REF
152
150
 
153
- def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
151
+
152
+ def narration_tempo_budget(tts_rate_offset=0.0):
154
153
  """Return the canonical tempo budget shared by voiceover and assemble."""
155
- cfg = config or CONFIG
156
- global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
157
- rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
158
- cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
159
- hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
160
- legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
154
+ global_speed = CONFIG["narration_speed"]
155
+ rate_factor = 1.0 + tts_rate_offset
156
+ cumulative_max = CONFIG["narration_cumulative_tempo_max"]
157
+ hard_max = max(cumulative_max, CONFIG["narration_cumulative_tempo_hard_max"])
158
+ legacy_segment_cap = CONFIG["tts_segment_tempo_max"]
161
159
  segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
162
160
  return {
163
161
  "global_narration_speed": global_speed,
@@ -168,43 +166,35 @@ def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
168
166
  "max_raw_duration_factor": global_speed * segment_tempo_max,
169
167
  }
170
168
 
169
+
171
170
  def log(msg):
172
171
  print(f"[video-recap] {msg}", flush=True)
173
172
 
173
+
174
174
  def run_cmd(cmd, **kwargs):
175
- """运行命令,返回 CompletedProcess"""
176
- if isinstance(cmd, list):
177
- display_parts = []
178
- for part in cmd:
179
- text = str(part)
180
- display_parts.append(text if len(text) <= 240 else text[:237] + "...")
181
- display = " ".join(display_parts)
182
- else:
183
- display = str(cmd)
184
- if len(display) > 2000:
185
- display = display[:1997] + "..."
175
+ """运行命令列表,返回 CompletedProcess"""
176
+ display = " ".join(
177
+ str(part) if len(str(part)) <= 240 else str(part)[:237] + "..." for part in cmd
178
+ )
186
179
  log(f"运行: {display}")
187
180
  return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
188
181
 
182
+
189
183
  def get_video_duration(video_path):
190
- """获取视频时长(秒)"""
191
- cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
184
+ """获取媒体时长(秒)"""
185
+ cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration",
192
186
  "-of", "csv=p=0", str(video_path)]
193
187
  result = run_cmd(cmd)
194
188
  if result.returncode != 0:
195
- return 0.0
196
- try:
197
- return float(result.stdout.strip())
198
- except (TypeError, ValueError):
199
- return 0.0
189
+ raise RuntimeError(f"ffprobe 无法读取时长: {video_path}: {result.stderr.strip()}")
190
+ return float(result.stdout.strip())
200
191
 
201
- def stable_json_dumps(value):
202
- """Serialize values deterministically for non-secret cache fingerprints."""
203
- return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
204
192
 
205
193
  def stable_hash(value):
206
- """Return an md5 digest for deterministic JSON-serializable values."""
207
- return hashlib.md5(stable_json_dumps(value).encode("utf-8")).hexdigest()
194
+ """Return an md5 digest of a deterministic JSON serialization (non-secret cache fingerprints)."""
195
+ encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
196
+ return hashlib.md5(encoded.encode("utf-8")).hexdigest()
197
+
208
198
 
209
199
  _FILE_FINGERPRINT_MEMO = {}
210
200
 
@@ -239,6 +229,8 @@ def file_fingerprint(path, chunk_size=1024 * 1024):
239
229
  digest = h.hexdigest()
240
230
  _FILE_FINGERPRINT_MEMO[key] = digest
241
231
  return digest
232
+
233
+
242
234
  def _retry_after_seconds(value, fallback):
243
235
  """Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
244
236
  if not value:
@@ -265,33 +257,22 @@ _ERROR_KEY_RE = re.compile(r"\b(?:tp|sk)-[A-Za-z0-9_-]{8,}\b")
265
257
 
266
258
  def _sanitize_api_error(value, limit=500, *, extra_secrets=()):
267
259
  """Bound transport diagnostics without echoing request media or credentials."""
268
- text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value or ""))
260
+ text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value))
269
261
  text = _ERROR_KEY_RE.sub("<redacted-key>", text)
270
262
  for secret in extra_secrets:
271
263
  if secret:
272
264
  text = text.replace(str(secret), "<redacted-key>")
273
265
  return text[:limit]
274
266
 
275
- def _api_headers(api_provider=None, api_url=None, api_key=None):
276
- """Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
277
- del api_provider, api_url # MiMo is the only provider; signature kept for call sites
278
- key = CONFIG.get("api_key", "") if api_key is None else api_key
279
- return {
280
- "Content-Type": "application/json",
281
- "User-Agent": "video-recap/1.0",
282
- "api-key": key,
283
- }
284
267
 
285
- def _prepare_api_payload(payload, api_provider=None, api_url=None):
268
+ def _prepare_api_payload(payload):
286
269
  """Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
287
- del api_provider, api_url
288
270
  normalized = dict(payload)
289
271
  if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
290
272
  normalized["max_completion_tokens"] = normalized.pop("max_tokens")
291
- model = str(normalized.get("model") or "")
292
273
  if (
293
- CONFIG.get("mimo_disable_thinking", True)
294
- and not model.endswith(("-tts", "-asr"))
274
+ CONFIG["mimo_disable_thinking"]
275
+ and not normalized["model"].endswith(("-tts", "-asr"))
295
276
  and "thinking" not in normalized
296
277
  ):
297
278
  # MiMo V2.5 may spend small max_completion_tokens budgets on reasoning_content.
@@ -299,57 +280,48 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
299
280
  normalized["thinking"] = {"type": "disabled"}
300
281
  return normalized
301
282
 
302
- def _mimo_endpoint(kind):
303
- """Return per-capability MiMo endpoint settings (video understanding / TTS / ASR)."""
304
- by_kind = {
305
- "video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_api_key_source"),
306
- "tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_api_key_source"),
307
- "asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_api_key_source"),
308
- }
309
- if kind not in by_kind:
310
- raise ValueError(f"Unsupported MiMo endpoint kind: {kind}")
311
- url_key, key_key, src_key = by_kind[kind]
312
- return {
313
- "api_url": CONFIG.get(url_key) or CONFIG.get("mimo_api_url"),
314
- "api_key": CONFIG.get(key_key) or CONFIG.get("mimo_api_key"),
315
- "api_key_source": CONFIG.get(src_key, "MIMO_API_KEY"),
316
- }
317
283
 
318
- def _call_mimo_endpoint(kind, payload, max_retries=10):
319
- settings = _mimo_endpoint(kind)
284
+ def mimo_tts_api_call(payload):
285
+ """Call the MiMo TTS endpoint."""
320
286
  return api_call(
321
287
  payload,
322
- max_retries=max_retries,
323
- api_provider="mimo",
324
- api_url=settings["api_url"],
325
- api_key=settings["api_key"],
326
- api_key_source=settings["api_key_source"],
288
+ max_retries=10,
289
+ api_url=CONFIG["mimo_tts_api_url"],
290
+ api_key=CONFIG["mimo_tts_api_key"],
291
+ api_key_source=CONFIG["mimo_tts_api_key_source"],
327
292
  )
328
293
 
329
- def mimo_tts_api_call(payload, max_retries=10):
330
- """Call the MiMo TTS endpoint."""
331
- return _call_mimo_endpoint("tts", payload, max_retries=max_retries)
332
294
 
333
- def mimo_asr_api_call(payload, max_retries=10):
295
+ def mimo_asr_api_call(payload):
334
296
  """Call the MiMo speech-recognition (ASR) endpoint."""
335
- return _call_mimo_endpoint("asr", payload, max_retries=max_retries)
297
+ return api_call(
298
+ payload,
299
+ max_retries=10,
300
+ api_url=CONFIG["mimo_api_url"],
301
+ api_key=CONFIG["mimo_api_key"],
302
+ api_key_source="MIMO_API_KEY",
303
+ )
336
304
 
337
- def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_key_source=None):
305
+
306
+ def api_call(payload, *, api_url, api_key, api_key_source, max_retries=8):
338
307
  """调用 OpenAI-compatible API,带重试。
339
308
 
340
309
  集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
341
310
  避免一次瞬时限流就中止整个阶段。配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s。
342
311
  """
343
- endpoint = normalize_api_url(api_url if api_url is not None else CONFIG["api_url"])
344
- headers = _api_headers(api_provider=api_provider, api_url=endpoint, api_key=api_key)
345
- data = json.dumps(_prepare_api_payload(payload, api_provider=api_provider, api_url=endpoint)).encode("utf-8")
312
+ endpoint = normalize_api_url(api_url)
313
+ headers = {
314
+ "Content-Type": "application/json",
315
+ "User-Agent": "video-recap/1.0",
316
+ "api-key": api_key,
317
+ }
318
+ data = json.dumps(_prepare_api_payload(payload)).encode("utf-8")
346
319
 
347
320
  for attempt in range(max_retries):
348
321
  try:
349
322
  req = urllib.request.Request(endpoint, data=data, headers=headers)
350
323
  with urllib.request.urlopen(req, timeout=300) as resp:
351
- result = json.loads(resp.read().decode("utf-8"))
352
- return result
324
+ return json.loads(resp.read().decode("utf-8"))
353
325
  except urllib.error.HTTPError as e:
354
326
  body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
355
327
  wait = min(2 ** attempt, 60)
@@ -358,8 +330,7 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
358
330
  wait = _retry_after_seconds(retry_after, max(wait, 10))
359
331
  log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
360
332
  elif e.code == 401:
361
- key_name = api_key_source or CONFIG.get("api_key_source", "MIMO_API_KEY")
362
- raise RuntimeError(f"API 认证失败 (401)。请检查 {key_name} 和 API URL 是否匹配。")
333
+ raise RuntimeError(f"API 认证失败 (401)。请检查 {api_key_source} 和 API URL 是否匹配。")
363
334
  elif e.code == 403:
364
335
  hint = "API 访问被拒绝 (403)。"
365
336
  if "1010" in body or "cloudflare" in body.lower():
@@ -382,8 +353,6 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
382
353
  else:
383
354
  raise RuntimeError(f"API 调用失败 {max_retries} 次: HTTP {e.code} — {body}")
384
355
  except Exception as e: # noqa: BLE001 - transport/decode faults are all retryable here
385
- # (Deliberately broad, but no longer written as `(URLError, Exception)`, which
386
- # read as a tuple while `Exception` already subsumed the first member.)
387
356
  wait = min(2 ** attempt, 60)
388
357
  safe_error = _sanitize_api_error(e)
389
358
  log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): {safe_error}")
@@ -392,13 +361,11 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
392
361
  time.sleep(wait)
393
362
  else:
394
363
  raise RuntimeError(f"API 调用失败 {max_retries} 次: {safe_error}")
395
- # Unreachable for max_retries >= 1; guards against a silent `None` return (and the
396
- # TypeError it would cause at the caller's resp["choices"]) if a caller passes 0.
397
- raise ValueError(f"max_retries must be >= 1, got {max_retries}")
364
+
398
365
 
399
366
  def _text_char_count(text):
400
367
  """计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
401
- return len(re.sub(r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]', '', text or ""))
368
+ return len(re.sub(r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]', '', text))
402
369
 
403
370
 
404
371
  def _truncate_at_sentence(text, max_chars):
@@ -11,7 +11,7 @@ import sys
11
11
  import wave
12
12
  from pathlib import Path
13
13
 
14
- from lib import CONFIG, log
14
+ from lib import CONFIG
15
15
 
16
16
 
17
17
  def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak_limit=0.98):
@@ -36,10 +36,12 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
36
36
  # OUTPUT just to measure it) — about 420ms for a 12s block, times every narration
37
37
  # segment. Same arithmetic, same rounding, one decode.
38
38
  samples = array.array("h")
39
- samples.frombytes(data[: len(data) - (len(data) % 2)])
39
+ samples.frombytes(data)
40
40
  if sys.byteorder != "little": # wave data is little-endian regardless of host
41
41
  samples.byteswap()
42
42
  if not samples:
43
+ # A silent block has no RMS to normalize toward; copy it through with neutral
44
+ # metadata rather than dividing by a zero sample count.
43
45
  output_wav.write_bytes(input_wav.read_bytes())
44
46
  return {
45
47
  "rms_dbfs_before": None,
@@ -51,10 +53,10 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
51
53
  rms = math.sqrt(sum(s * s for s in samples) / count)
52
54
  peak = max(max(samples), -min(samples)) / 32768.0
53
55
  rms_dbfs_before = 20 * math.log10(max(rms, 1e-9) / 32768.0)
54
- target_linear = 10 ** (float(target_rms_dbfs) / 20.0) * 32768.0
56
+ target_linear = 10 ** (target_rms_dbfs / 20.0) * 32768.0
55
57
  gain = target_linear / max(rms, 1e-9)
56
58
  if peak > 0:
57
- gain = min(gain, float(peak_limit) / peak)
59
+ gain = min(gain, peak_limit / peak)
58
60
 
59
61
  # Accumulate the output statistics while normalizing instead of decoding the result back.
60
62
  normalized = array.array("h", bytes(2 * count))
@@ -64,8 +66,7 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
64
66
  value = max(-32768, min(32767, int(round(samples[i] * gain))))
65
67
  normalized[i] = value
66
68
  out_square_sum += value * value
67
- if abs(value) > out_peak_raw:
68
- out_peak_raw = abs(value)
69
+ out_peak_raw = max(out_peak_raw, abs(value))
69
70
  out_bytes = normalized
70
71
  if sys.byteorder != "little":
71
72
  out_bytes = array.array("h", normalized)
@@ -86,28 +87,16 @@ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak
86
87
 
87
88
 
88
89
  def _maybe_normalize_tts_wav(output_wav):
89
- """Normalize a synthesized TTS block in-place when possible.
90
-
91
- Unit tests often stub TTS with text bytes rather than real WAV. In that case
92
- normalization is skipped safely; real MiMo WAV output gets RMS/peak metadata.
93
- """
94
- if not CONFIG.get("tts_segment_normalize", True):
90
+ """Normalize a synthesized TTS block in-place; None when normalization is disabled."""
91
+ if not CONFIG["tts_segment_normalize"]:
95
92
  return None
96
93
  output_wav = Path(output_wav)
97
94
  tmp = output_wav.with_name(f"{output_wav.stem}_norm{output_wav.suffix}")
98
- try:
99
- meta = _normalize_tts_wav_rms(
100
- output_wav,
101
- tmp,
102
- target_rms_dbfs=float(CONFIG.get("tts_segment_target_rms_dbfs", -20.0) or -20.0),
103
- peak_limit=float(CONFIG.get("tts_segment_peak_limit", 0.98) or 0.98),
104
- )
105
- os.replace(tmp, output_wav)
106
- return meta
107
- except Exception as exc:
108
- try:
109
- tmp.unlink(missing_ok=True)
110
- except OSError:
111
- pass
112
- log(f" TTS RMS 归一跳过: {exc}")
113
- return None
95
+ meta = _normalize_tts_wav_rms(
96
+ output_wav,
97
+ tmp,
98
+ target_rms_dbfs=CONFIG["tts_segment_target_rms_dbfs"],
99
+ peak_limit=CONFIG["tts_segment_peak_limit"],
100
+ )
101
+ os.replace(tmp, output_wav)
102
+ return meta