@oh-story/dsh 0.1.4 → 0.1.6

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 (479) hide show
  1. package/README.md +56 -12
  2. package/lib/client.js +2707 -268
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +136 -101
  5. package/lib/drama/skills/short-drama/SKILL.md +61 -1
  6. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
  8. package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
  9. package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
  10. package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
  11. package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
  12. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  13. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  14. package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
  15. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  16. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
  17. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
  18. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  19. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
  21. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
  22. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
  23. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
  24. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
  25. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  26. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  27. package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
  28. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  29. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  30. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  31. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  32. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  33. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  34. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
  35. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  36. package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
  37. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  38. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
  39. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  40. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
  41. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
  42. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
  43. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  44. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
  45. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  46. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  47. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
  50. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
  51. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  53. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
  54. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  55. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  56. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  57. package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
  58. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  59. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  60. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  61. package/lib/index.js +865 -33
  62. package/lib/index.js.map +4 -4
  63. package/lib/novel-to-game/LICENSE +21 -0
  64. package/lib/novel-to-game/README.md +180 -0
  65. package/lib/novel-to-game/README_ZH.md +178 -0
  66. package/lib/novel-to-game/VERSION +1 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  91. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  92. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  93. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  94. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  95. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  96. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  97. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  98. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  99. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  100. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  101. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  102. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  103. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  104. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  128. package/lib/novel-to-game/manifest.json +508 -0
  129. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  130. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  131. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  132. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  133. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  134. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  135. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  136. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  137. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  138. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  139. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  140. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  141. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  142. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  143. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  144. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  145. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  146. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  147. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  148. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  149. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  150. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  151. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  152. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  153. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  154. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  155. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  156. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  157. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  158. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  159. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  160. package/lib/oh-story/manifest.json +415 -305
  161. package/lib/oh-story/roles/consistency-checker.md +1 -1
  162. package/lib/oh-story/roles/narrative-writer.md +32 -33
  163. package/lib/oh-story/roles/story-architect.md +16 -26
  164. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  165. package/lib/oh-story/skills/story/SKILL.md +16 -9
  166. package/lib/oh-story/skills/story/VERSION +1 -1
  167. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  168. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  169. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  170. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  171. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  172. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  173. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  174. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  175. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  176. package/lib/oh-story/skills/story-import/SKILL.md +19 -10
  177. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  180. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  181. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  182. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  183. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  184. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  185. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  186. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  187. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  188. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  189. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  190. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  191. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  192. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  193. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  194. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  195. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  196. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  197. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  198. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
  199. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
  200. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  201. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  202. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  203. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  204. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  205. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  206. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
  207. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  208. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
  209. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  210. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  211. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  212. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  213. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  214. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  215. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  216. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  217. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  218. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  219. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  220. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  221. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  222. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  223. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  224. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  225. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  226. package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
  227. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  228. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  229. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  230. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  236. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  237. package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
  238. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  239. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  243. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  244. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  245. package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
  246. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  247. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  248. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  249. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  250. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  251. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  252. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  253. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  254. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  255. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  257. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  258. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  259. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  260. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  261. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  262. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  263. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  264. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  265. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  266. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  267. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  268. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  270. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  271. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  272. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  273. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  274. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  276. 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
  277. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
  278. 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
  279. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  280. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
  281. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
  282. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  283. 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
  284. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  285. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  286. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  287. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  288. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  289. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  290. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  291. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  292. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  293. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  294. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  295. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  296. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  297. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  298. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  299. package/lib/types/client/drama-production-view.d.ts +34 -0
  300. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  301. package/lib/types/client/drama-production.d.ts +92 -0
  302. package/lib/types/client/drama-production.d.ts.map +1 -0
  303. package/lib/types/client/file-activity.d.ts +4 -2
  304. package/lib/types/client/file-activity.d.ts.map +1 -1
  305. package/lib/types/client/index.d.ts +1 -1
  306. package/lib/types/client/index.d.ts.map +1 -1
  307. package/lib/types/client/production-intents.d.ts +9 -0
  308. package/lib/types/client/production-intents.d.ts.map +1 -0
  309. package/lib/types/client/production-prompts.d.ts +9 -0
  310. package/lib/types/client/production-prompts.d.ts.map +1 -0
  311. package/lib/types/client/production-runtime.d.ts +58 -0
  312. package/lib/types/client/production-runtime.d.ts.map +1 -0
  313. package/lib/types/client/video-studio.d.ts +41 -0
  314. package/lib/types/client/video-studio.d.ts.map +1 -0
  315. package/lib/types/client/workbench-ui.d.ts +5 -0
  316. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  317. package/lib/types/game-verification.d.ts +16 -0
  318. package/lib/types/game-verification.d.ts.map +1 -0
  319. package/lib/types/index.d.ts +3 -1
  320. package/lib/types/index.d.ts.map +1 -1
  321. package/lib/types/production-intent.d.ts +21 -0
  322. package/lib/types/production-intent.d.ts.map +1 -0
  323. package/lib/types/production-tool.d.ts +5 -0
  324. package/lib/types/production-tool.d.ts.map +1 -0
  325. package/lib/types/role-tool.d.ts.map +1 -1
  326. package/lib/types/skill-provider.d.ts +7 -0
  327. package/lib/types/skill-provider.d.ts.map +1 -1
  328. package/lib/types/video-project.d.ts +42 -0
  329. package/lib/types/video-project.d.ts.map +1 -0
  330. package/lib/types/workspace-request-trust.d.ts +6 -0
  331. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  332. package/lib/types/workspace-route.d.ts +20 -0
  333. package/lib/types/workspace-route.d.ts.map +1 -1
  334. package/lib/video-recap/CHANGELOG.md +282 -0
  335. package/lib/video-recap/LICENSE +21 -0
  336. package/lib/video-recap/README.en.md +257 -0
  337. package/lib/video-recap/README.md +255 -0
  338. package/lib/video-recap/manifest.json +684 -0
  339. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  340. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  341. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  342. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  343. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  344. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  345. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  346. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  347. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  348. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  349. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  350. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  351. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  352. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  353. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  354. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  355. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  356. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  357. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  358. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  359. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  360. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  361. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  362. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  363. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  364. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  365. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  366. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  367. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  368. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  369. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  370. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  371. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  372. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  373. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  374. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  375. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  376. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  377. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  378. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  379. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  380. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  381. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  382. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  383. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  384. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  385. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  386. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  387. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  388. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  389. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  390. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  391. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  392. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  393. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  394. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  395. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  396. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  397. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  398. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  399. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  400. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  401. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  402. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  403. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  404. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  405. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  406. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  407. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  408. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  409. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  410. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  411. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  412. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  413. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  414. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  415. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  416. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  417. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  418. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  419. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  420. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  421. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  422. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  423. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  424. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  425. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  426. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  427. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  428. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  429. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  430. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  431. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  432. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  433. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  434. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  435. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  436. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  437. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  438. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  439. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  440. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  441. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  442. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  443. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  444. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  445. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  446. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  447. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  448. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  449. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  450. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  451. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  452. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  453. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  454. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  455. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  456. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  457. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  458. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  459. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  460. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  461. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  462. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  463. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  464. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  465. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  466. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  467. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  468. package/package.json +55 -40
  469. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  470. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  471. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  472. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  473. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  474. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  475. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  476. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  477. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  478. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  479. /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
@@ -0,0 +1,109 @@
1
+ """Fish Audio TTS transport for the video-voiceover skill.
2
+
3
+ The provider returns audio bytes rather than JSON. Keep the request, response validation,
4
+ and secret-safe error handling isolated here so voiceover.py only selects an engine.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import urllib.error
10
+ import urllib.request
11
+ from pathlib import Path
12
+
13
+ from lib import (
14
+ CONFIG,
15
+ DEFAULT_FISH_TTS_API_URL,
16
+ DEFAULT_FISH_TTS_MODEL,
17
+ _sanitize_api_error,
18
+ )
19
+
20
+
21
+ def _fish_payload(text, rate="+0%"):
22
+ try:
23
+ speed = 1.0 + float(str(rate).rstrip("%")) / 100.0
24
+ except (TypeError, ValueError):
25
+ speed = 1.0
26
+ payload = {
27
+ "text": text,
28
+ "format": "wav",
29
+ "normalize": True,
30
+ "prosody": {
31
+ "speed": max(0.5, min(2.0, speed)),
32
+ "volume": 0,
33
+ "normalize_loudness": True,
34
+ },
35
+ }
36
+ reference_id = str(CONFIG.get("fish_tts_reference_id") or "").strip()
37
+ if reference_id:
38
+ payload["reference_id"] = reference_id
39
+ return payload
40
+
41
+
42
+ def synthesize_fish_audio(text, output_path, rate="+0%", **_ignored):
43
+ """Synthesize one narration block and atomically write the WAV response."""
44
+ api_key = str(CONFIG.get("fish_api_key") or "").strip()
45
+ if not api_key:
46
+ raise RuntimeError("请设置 FISH_API_KEY 用于 Fish Audio TTS")
47
+
48
+ endpoint = str(CONFIG.get("fish_tts_api_url") or DEFAULT_FISH_TTS_API_URL)
49
+ model = str(CONFIG.get("fish_tts_model") or DEFAULT_FISH_TTS_MODEL)
50
+ body = json.dumps(_fish_payload(text, rate), ensure_ascii=False).encode("utf-8")
51
+ request = urllib.request.Request(
52
+ endpoint,
53
+ data=body,
54
+ headers={
55
+ "Authorization": f"Bearer {api_key}",
56
+ "Content-Type": "application/json",
57
+ "Accept": "audio/wav",
58
+ "model": model,
59
+ "User-Agent": "video-recap/1.0",
60
+ },
61
+ method="POST",
62
+ )
63
+
64
+ timeout = max(1, int(CONFIG.get("tts_timeout", 300) or 300))
65
+ try:
66
+ with urllib.request.urlopen(request, timeout=timeout) as response:
67
+ audio = response.read()
68
+ content_type = str(response.headers.get("Content-Type") or "").lower()
69
+ except urllib.error.HTTPError as exc:
70
+ detail = _sanitize_api_error(
71
+ exc.read().decode("utf-8", errors="replace"),
72
+ extra_secrets=(api_key,),
73
+ )
74
+ if exc.code == 401:
75
+ raise RuntimeError("Fish Audio 认证失败 (401),请检查 FISH_API_KEY") from exc
76
+ if exc.code == 402:
77
+ raise RuntimeError("Fish Audio 请求被拒绝 (402),请检查模型额度或计费状态") from exc
78
+ raise RuntimeError(f"Fish Audio TTS 请求失败 (HTTP {exc.code}): {detail}") from exc
79
+ except urllib.error.URLError as exc:
80
+ raise RuntimeError(
81
+ "Fish Audio TTS 网络错误: "
82
+ f"{_sanitize_api_error(exc.reason, extra_secrets=(api_key,))}"
83
+ ) from exc
84
+
85
+ if "json" in content_type:
86
+ detail = _sanitize_api_error(
87
+ audio.decode("utf-8", errors="replace"),
88
+ extra_secrets=(api_key,),
89
+ )
90
+ raise RuntimeError(
91
+ f"Fish Audio TTS 返回了 JSON 而不是音频: {detail}"
92
+ )
93
+ if (
94
+ len(audio) <= 44
95
+ or audio[:4] != b"RIFF"
96
+ or audio[8:12] != b"WAVE"
97
+ ):
98
+ raise RuntimeError("Fish Audio TTS 返回的内容不是有效 WAV")
99
+
100
+ output = Path(output_path)
101
+ partial = Path(str(output) + ".part")
102
+ try:
103
+ partial.write_bytes(audio)
104
+ os.replace(partial, output)
105
+ finally:
106
+ try:
107
+ partial.unlink(missing_ok=True)
108
+ except OSError:
109
+ pass
@@ -0,0 +1,421 @@
1
+ """Self-contained config + utilities for this skill (no cross-skill imports).
2
+ Merged from the shared core; reads the same env vars as the rest of the bundle."""
3
+ import json
4
+ import hashlib
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import time
9
+ import urllib.request
10
+ import urllib.error
11
+ from datetime import datetime, timezone
12
+ from email.utils import parsedate_to_datetime
13
+
14
+
15
+ # ── 配置 ──────────────────────────────────────────────────────────────
16
+ _EXISTING_CONFIG_REF = globals().get("CONFIG")
17
+
18
+ DEFAULT_MIMO_API_URL = "https://api.xiaomimimo.com/v1"
19
+ DEFAULT_MIMO_TOKEN_PLAN_CLUSTER = "cn"
20
+ MIMO_TOKEN_PLAN_API_URLS = {
21
+ "cn": "https://token-plan-cn.xiaomimimo.com/v1",
22
+ "sgp": "https://token-plan-sgp.xiaomimimo.com/v1",
23
+ "ams": "https://token-plan-ams.xiaomimimo.com/v1",
24
+ }
25
+ DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
26
+ DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
27
+ DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
28
+ DEFAULT_FISH_TTS_API_URL = "https://api.fish.audio/v1/tts"
29
+ DEFAULT_FISH_TTS_MODEL = "s2.1-pro-free"
30
+ DEFAULT_FISH_TTS_REFERENCE_ID = "5653cea4ac83480aaf2bf45406556185"
31
+
32
+
33
+ def normalize_api_url(raw_url):
34
+ """Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
35
+ url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
36
+ if url.endswith("/chat/completions"):
37
+ return url
38
+ return f"{url}/chat/completions"
39
+
40
+
41
+ def is_mimo_token_plan_key(api_key):
42
+ """Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
43
+ return str(api_key or "").strip().startswith("tp-")
44
+
45
+
46
+ def default_mimo_api_url(api_key="", cluster=None):
47
+ """Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
48
+
49
+ MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
50
+ (`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
51
+ not the pay-as-you-go `api.xiaomimimo.com` endpoint.
52
+ """
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
58
+
59
+
60
+ def env_int(name, default, *, minimum=None):
61
+ """Read an integer env var; ignore malformed values instead of crashing import."""
62
+ raw = os.environ.get(name)
63
+ if raw is None or raw == "":
64
+ return default
65
+ try:
66
+ value = int(raw)
67
+ except (TypeError, ValueError):
68
+ return default
69
+ if minimum is not None:
70
+ value = max(minimum, value)
71
+ return value
72
+
73
+
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"}
80
+
81
+
82
+ def env_float(name, default, *, minimum=None):
83
+ """Read a float env var; ignore malformed values instead of crashing import."""
84
+ raw = os.environ.get(name)
85
+ if raw is None or raw == "":
86
+ 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
+
95
+
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.
100
+ _mimo_api_key = os.environ.get("MIMO_API_KEY", "")
101
+ _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)
103
+ _raw_mimo_tts_api_url = (
104
+ os.environ.get("MIMO_TTS_API_URL")
105
+ or os.environ.get("MIMO_API_URL")
106
+ or default_mimo_api_url(_mimo_tts_api_key)
107
+ )
108
+
109
+ CONFIG = {
110
+ "api_url": normalize_api_url(_raw_api_url),
111
+ "api_key": _mimo_api_key,
112
+ "api_key_source": "MIMO_API_KEY",
113
+ "mimo_api_url": normalize_api_url(_raw_api_url),
114
+ "mimo_api_key": _mimo_api_key,
115
+ "mimo_tts_api_url": normalize_api_url(_raw_mimo_tts_api_url),
116
+ "mimo_tts_api_key": _mimo_tts_api_key,
117
+ "mimo_tts_api_key_source": "MIMO_TTS_API_KEY" if os.environ.get("MIMO_TTS_API_KEY") else "MIMO_API_KEY",
118
+ "mimo_asr_model": os.environ.get("MIMO_ASR_MODEL", DEFAULT_MIMO_ASR_MODEL),
119
+ "mimo_tts_model": os.environ.get("MIMO_TTS_MODEL", DEFAULT_MIMO_TTS_MODEL),
120
+ "mimo_tts_voice": os.environ.get("MIMO_TTS_VOICE", "冰糖"),
121
+ "voice_ref": os.environ.get("VOICE_REF", "").strip(), # optional arbitrary reference audio for narration voice clone
122
+ "mimo_tts_style": os.environ.get(
123
+ "MIMO_TTS_STYLE",
124
+ "自然、清晰、有感染力,像在给观众讲故事;随剧情起伏,该紧张时紧张、该动情时动情,不平铺直叙。",
125
+ ),
126
+ "tts_provider": os.environ.get("TTS_PROVIDER", "auto").strip().lower(),
127
+ "tts_timeout": env_int("TTS_TIMEOUT", 300, minimum=1),
128
+ "fish_api_key": os.environ.get("FISH_API_KEY", ""),
129
+ "fish_tts_api_url": os.environ.get("FISH_TTS_API_URL", DEFAULT_FISH_TTS_API_URL),
130
+ "fish_tts_model": os.environ.get("FISH_TTS_MODEL", DEFAULT_FISH_TTS_MODEL),
131
+ "fish_tts_reference_id": os.environ.get(
132
+ "FISH_TTS_REFERENCE_ID", DEFAULT_FISH_TTS_REFERENCE_ID
133
+ ).strip(),
134
+ "mimo_disable_thinking": env_bool("MIMO_DISABLE_THINKING", True),
135
+ "breath_ms": 250, # 段间呼吸空间(ms);block recap 块内连贯、块间留原声呼吸
136
+ "narration_speed": env_float("NARRATION_SPEED", 1.15, minimum=0.5), # 解说整体提速(atempo),默认回到可懂区间;长片可设 1.0
137
+ "narration_cumulative_tempo_max": env_float("NARRATION_CUMULATIVE_TEMPO_MAX", 1.35, minimum=1.0), # TTS rate × 全局 atempo × 段内 atempo 的累计上限
138
+ "narration_cumulative_tempo_hard_max": env_float("NARRATION_CUMULATIVE_TEMPO_HARD_MAX", 1.40, minimum=1.0), # QC/阻断硬上限
139
+ "tts_segment_tempo_max": env_float("TTS_SEGMENT_TEMPO_MAX", 1.20, minimum=1.0), # 兼容旧段内 atempo 上限;实际会被累计预算收紧
140
+ "tts_dynamic_params": True, # 启用动态语速调节
141
+ "tts_workers": env_int("TTS_WORKERS", 4, minimum=1), # TTS 并行合成线程数
142
+ "tts_retries": env_int("TTS_RETRIES", 3, minimum=1), # 单段 TTS 失败重试次数
143
+ "allow_partial_tts": env_bool("ALLOW_PARTIAL_TTS", False),
144
+ "tts_segment_normalize": env_bool("TTS_SEGMENT_NORMALIZE", True), # 单段 TTS RMS 归一,降低段间忽大忽小
145
+ "tts_segment_target_rms_dbfs": env_float("TTS_SEGMENT_TARGET_RMS_DBFS", -20.0),
146
+ "tts_segment_peak_limit": env_float("TTS_SEGMENT_PEAK_LIMIT", 0.98, minimum=0.1),
147
+ }
148
+ if isinstance(_EXISTING_CONFIG_REF, dict):
149
+ _EXISTING_CONFIG_REF.clear()
150
+ _EXISTING_CONFIG_REF.update(CONFIG)
151
+ CONFIG = _EXISTING_CONFIG_REF
152
+
153
+ def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
154
+ """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))
161
+ segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
162
+ return {
163
+ "global_narration_speed": global_speed,
164
+ "tts_rate_factor": rate_factor,
165
+ "cumulative_tempo_max": cumulative_max,
166
+ "cumulative_tempo_hard_max": hard_max,
167
+ "segment_tempo_max": segment_tempo_max,
168
+ "max_raw_duration_factor": global_speed * segment_tempo_max,
169
+ }
170
+
171
+ def log(msg):
172
+ print(f"[video-recap] {msg}", flush=True)
173
+
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] + "..."
186
+ log(f"运行: {display}")
187
+ return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
188
+
189
+ def get_video_duration(video_path):
190
+ """获取视频时长(秒)"""
191
+ cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
192
+ "-of", "csv=p=0", str(video_path)]
193
+ result = run_cmd(cmd)
194
+ if result.returncode != 0:
195
+ return 0.0
196
+ try:
197
+ return float(result.stdout.strip())
198
+ except (TypeError, ValueError):
199
+ return 0.0
200
+
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
+
205
+ 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()
208
+
209
+ _FILE_FINGERPRINT_MEMO = {}
210
+
211
+
212
+ def _file_identity(path):
213
+ """(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
214
+ st = os.stat(os.fspath(path))
215
+ return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
216
+
217
+
218
+ def file_fingerprint(path, chunk_size=1024 * 1024):
219
+ """Return a full-content fingerprint for cache-correct identity checks.
220
+
221
+ The digest covers CONTENT only — never the path or mtime — so a copied video or
222
+ artifact is still recognised as the same asset, while any byte change invalidates
223
+ the cache even if timestamps, size, head, or tail bytes are misleading.
224
+
225
+ Identity metadata is used ONLY to memoize within a single process. One understanding
226
+ run fingerprints the same source video 8-10 times and the whole extracted frame set
227
+ 2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
228
+ any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
229
+ re-hashed, so the memo can never serve a stale digest.
230
+ """
231
+ key = _file_identity(path)
232
+ memoized = _FILE_FINGERPRINT_MEMO.get(key)
233
+ if memoized is not None:
234
+ return memoized
235
+ h = hashlib.sha256()
236
+ with open(os.fspath(path), "rb") as f:
237
+ for chunk in iter(lambda: f.read(chunk_size), b""):
238
+ h.update(chunk)
239
+ digest = h.hexdigest()
240
+ _FILE_FINGERPRINT_MEMO[key] = digest
241
+ return digest
242
+ def _retry_after_seconds(value, fallback):
243
+ """Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
244
+ if not value:
245
+ return fallback
246
+ try:
247
+ return max(fallback, max(0, int(value)))
248
+ except (TypeError, ValueError):
249
+ pass
250
+ try:
251
+ retry_at = parsedate_to_datetime(value)
252
+ if retry_at.tzinfo is None:
253
+ retry_at = retry_at.replace(tzinfo=timezone.utc)
254
+ return max(fallback, max(0, int((retry_at - datetime.now(timezone.utc)).total_seconds())))
255
+ except (TypeError, ValueError, IndexError, OverflowError):
256
+ return fallback
257
+
258
+
259
+ _ERROR_DATA_URL_RE = re.compile(
260
+ r"data:(?:audio|video|image)/[^;,\s\"'<>]+;base64,[A-Za-z0-9+/=]+",
261
+ re.IGNORECASE,
262
+ )
263
+ _ERROR_KEY_RE = re.compile(r"\b(?:tp|sk)-[A-Za-z0-9_-]{8,}\b")
264
+
265
+
266
+ def _sanitize_api_error(value, limit=500, *, extra_secrets=()):
267
+ """Bound transport diagnostics without echoing request media or credentials."""
268
+ text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value or ""))
269
+ text = _ERROR_KEY_RE.sub("<redacted-key>", text)
270
+ for secret in extra_secrets:
271
+ if secret:
272
+ text = text.replace(str(secret), "<redacted-key>")
273
+ return text[:limit]
274
+
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
+
285
+ def _prepare_api_payload(payload, api_provider=None, api_url=None):
286
+ """Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
287
+ del api_provider, api_url
288
+ normalized = dict(payload)
289
+ if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
290
+ normalized["max_completion_tokens"] = normalized.pop("max_tokens")
291
+ model = str(normalized.get("model") or "")
292
+ if (
293
+ CONFIG.get("mimo_disable_thinking", True)
294
+ and not model.endswith(("-tts", "-asr"))
295
+ and "thinking" not in normalized
296
+ ):
297
+ # MiMo V2.5 may spend small max_completion_tokens budgets on reasoning_content.
298
+ # The recap pipeline needs visible text, so disable thinking unless set explicitly.
299
+ normalized["thinking"] = {"type": "disabled"}
300
+ return normalized
301
+
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
+
318
+ def _call_mimo_endpoint(kind, payload, max_retries=10):
319
+ settings = _mimo_endpoint(kind)
320
+ return api_call(
321
+ 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"],
327
+ )
328
+
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
+
333
+ def mimo_asr_api_call(payload, max_retries=10):
334
+ """Call the MiMo speech-recognition (ASR) endpoint."""
335
+ return _call_mimo_endpoint("asr", payload, max_retries=max_retries)
336
+
337
+ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_key_source=None):
338
+ """调用 OpenAI-compatible API,带重试。
339
+
340
+ 集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
341
+ 避免一次瞬时限流就中止整个阶段。配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s。
342
+ """
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")
346
+
347
+ for attempt in range(max_retries):
348
+ try:
349
+ req = urllib.request.Request(endpoint, data=data, headers=headers)
350
+ with urllib.request.urlopen(req, timeout=300) as resp:
351
+ result = json.loads(resp.read().decode("utf-8"))
352
+ return result
353
+ except urllib.error.HTTPError as e:
354
+ body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
355
+ wait = min(2 ** attempt, 60)
356
+ if e.code == 429:
357
+ retry_after = e.headers.get("Retry-After")
358
+ wait = _retry_after_seconds(retry_after, max(wait, 10))
359
+ log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
360
+ 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 是否匹配。")
363
+ elif e.code == 403:
364
+ hint = "API 访问被拒绝 (403)。"
365
+ if "1010" in body or "cloudflare" in body.lower():
366
+ hint += "IP 被 Cloudflare 限流,请等待几分钟后重试。"
367
+ raise RuntimeError(hint)
368
+ hint += "请检查 API key 权限和 API URL 设置。"
369
+ raise RuntimeError(hint)
370
+ elif e.code == 405:
371
+ raise RuntimeError("API 端点不可用 (405),可能被 WAF 拦截。请检查 MIMO_API_URL 或稍后重试。")
372
+ elif e.code == 503:
373
+ log(f"API 服务暂不可用 (503),等待 {wait}s (尝试 {attempt+1}/{max_retries})")
374
+ elif e.code == 524:
375
+ # Cloudflare 超时:服务端处理超时,需要更长退避
376
+ wait = max(wait, 4 * (attempt + 1))
377
+ log(f"API 超时 (524),等待 {wait}s (尝试 {attempt+1}/{max_retries})")
378
+ else:
379
+ log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): HTTP {e.code} — {body}")
380
+ if attempt < max_retries - 1:
381
+ time.sleep(wait)
382
+ else:
383
+ raise RuntimeError(f"API 调用失败 {max_retries} 次: HTTP {e.code} — {body}")
384
+ 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
+ wait = min(2 ** attempt, 60)
388
+ safe_error = _sanitize_api_error(e)
389
+ log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): {safe_error}")
390
+ if attempt < max_retries - 1:
391
+ log(f"等待 {wait}s 后重试...")
392
+ time.sleep(wait)
393
+ else:
394
+ 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}")
398
+
399
+ def _text_char_count(text):
400
+ """计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
401
+ return len(re.sub(r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]', '', text or ""))
402
+
403
+
404
+ def _truncate_at_sentence(text, max_chars):
405
+ """在句子边界截断,不产生残句。max_chars 按有效字符计(不含标点空白)。"""
406
+ if _text_char_count(text) <= max_chars:
407
+ return text
408
+ eff = 0
409
+ cutoff = len(text)
410
+ for i, ch in enumerate(text):
411
+ eff += 1 if _text_char_count(ch) else 0
412
+ if eff > max_chars:
413
+ cutoff = i + 1
414
+ break
415
+ idx = max(text[:cutoff].rfind(sep) for sep in ['。', '!', '?', '!', '?'])
416
+ if idx > 0:
417
+ return text[:idx + 1]
418
+ idx = max(text[:cutoff].rfind(sep) for sep in [',', '、', ';', ','])
419
+ if idx > 3:
420
+ return text[:idx] + '。'
421
+ return ""
@@ -0,0 +1,113 @@
1
+ """Loudness normalization for synthesized TTS blocks.
2
+
3
+ Split out of voiceover.py to keep that module within the bundle's per-module line
4
+ budget. Dependency-free on purpose (stdlib `wave` + `array` only) so QC and assembly
5
+ can reuse the returned metadata without pulling in the synthesis path.
6
+ """
7
+ import array
8
+ import math
9
+ import os
10
+ import sys
11
+ import wave
12
+ from pathlib import Path
13
+
14
+ from lib import CONFIG, log
15
+
16
+
17
+ def _normalize_tts_wav_rms(input_wav, output_wav, *, target_rms_dbfs=-20.0, peak_limit=0.98):
18
+ """Normalize a mono/stereo 16-bit WAV to a target RMS with peak guard.
19
+
20
+ This helper is intentionally dependency-free so QC/assembly can reuse the
21
+ returned metadata even when normalization is applied in a later lane.
22
+ """
23
+ input_wav = Path(input_wav)
24
+ output_wav = Path(output_wav)
25
+ with wave.open(str(input_wav), "rb") as wf:
26
+ channels = wf.getnchannels()
27
+ sampwidth = wf.getsampwidth()
28
+ framerate = wf.getframerate()
29
+ frames = wf.getnframes()
30
+ data = wf.readframes(frames)
31
+ if sampwidth != 2:
32
+ raise ValueError(f"仅支持 16-bit PCM WAV: {input_wav}")
33
+
34
+ # array('h') decodes the whole block in C. The previous per-sample int.from_bytes
35
+ # comprehension ran three times over the audio (decode, normalize, re-decode the
36
+ # OUTPUT just to measure it) — about 420ms for a 12s block, times every narration
37
+ # segment. Same arithmetic, same rounding, one decode.
38
+ samples = array.array("h")
39
+ samples.frombytes(data[: len(data) - (len(data) % 2)])
40
+ if sys.byteorder != "little": # wave data is little-endian regardless of host
41
+ samples.byteswap()
42
+ if not samples:
43
+ output_wav.write_bytes(input_wav.read_bytes())
44
+ return {
45
+ "rms_dbfs_before": None,
46
+ "rms_dbfs_after": None,
47
+ "peak_after": 0.0,
48
+ "gain_db": 0.0,
49
+ }
50
+ count = len(samples)
51
+ rms = math.sqrt(sum(s * s for s in samples) / count)
52
+ peak = max(max(samples), -min(samples)) / 32768.0
53
+ rms_dbfs_before = 20 * math.log10(max(rms, 1e-9) / 32768.0)
54
+ target_linear = 10 ** (float(target_rms_dbfs) / 20.0) * 32768.0
55
+ gain = target_linear / max(rms, 1e-9)
56
+ if peak > 0:
57
+ gain = min(gain, float(peak_limit) / peak)
58
+
59
+ # Accumulate the output statistics while normalizing instead of decoding the result back.
60
+ normalized = array.array("h", bytes(2 * count))
61
+ out_square_sum = 0
62
+ out_peak_raw = 0
63
+ for i in range(count):
64
+ value = max(-32768, min(32767, int(round(samples[i] * gain))))
65
+ normalized[i] = value
66
+ out_square_sum += value * value
67
+ if abs(value) > out_peak_raw:
68
+ out_peak_raw = abs(value)
69
+ out_bytes = normalized
70
+ if sys.byteorder != "little":
71
+ out_bytes = array.array("h", normalized)
72
+ out_bytes.byteswap()
73
+ with wave.open(str(output_wav), "wb") as wf:
74
+ wf.setnchannels(channels)
75
+ wf.setsampwidth(sampwidth)
76
+ wf.setframerate(framerate)
77
+ wf.writeframes(out_bytes.tobytes())
78
+
79
+ out_rms = math.sqrt(out_square_sum / count)
80
+ return {
81
+ "rms_dbfs_before": rms_dbfs_before,
82
+ "rms_dbfs_after": 20 * math.log10(max(out_rms, 1e-9) / 32768.0),
83
+ "peak_after": out_peak_raw / 32768.0,
84
+ "gain_db": 20 * math.log10(max(gain, 1e-9)),
85
+ }
86
+
87
+
88
+ 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):
95
+ return None
96
+ output_wav = Path(output_wav)
97
+ 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