@oh-story/dsh 0.1.10 → 0.1.12

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 (444) hide show
  1. package/README.md +11 -11
  2. package/lib/client.js +7 -3
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +212 -142
  5. package/lib/drama/skills/short-drama/SKILL.md +14 -2
  6. package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
  8. package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
  9. package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
  10. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
  11. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
  12. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
  13. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  14. package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
  15. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
  16. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
  17. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
  18. package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
  19. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
  20. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
  21. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
  22. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
  23. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
  24. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
  25. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
  26. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
  27. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
  28. package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
  29. package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
  30. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
  31. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
  32. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
  33. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
  34. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
  35. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
  36. package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
  37. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
  38. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
  39. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
  40. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
  41. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
  42. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
  43. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
  44. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
  45. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
  46. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
  47. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
  48. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
  49. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
  50. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
  51. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
  52. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
  53. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
  54. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
  55. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
  56. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
  57. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
  58. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
  59. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
  60. package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
  61. package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
  62. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
  63. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
  64. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
  65. package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
  66. package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
  67. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
  68. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
  69. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
  70. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
  71. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
  72. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
  73. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
  74. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
  75. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
  76. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
  77. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
  78. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
  79. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
  80. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  81. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
  82. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
  83. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
  84. package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
  85. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
  86. package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
  87. package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
  88. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
  89. package/lib/index.js +71 -21
  90. package/lib/index.js.map +3 -3
  91. package/lib/oh-story/manifest.json +545 -310
  92. package/lib/oh-story/roles/chapter-extractor.md +11 -11
  93. package/lib/oh-story/roles/character-designer.md +1 -2
  94. package/lib/oh-story/roles/consistency-checker.md +11 -9
  95. package/lib/oh-story/roles/narrative-writer.md +24 -25
  96. package/lib/oh-story/roles/story-architect.md +9 -61
  97. package/lib/oh-story/roles/story-explorer.md +20 -125
  98. package/lib/oh-story/roles/story-researcher.md +0 -1
  99. package/lib/oh-story/skills/story/SKILL.md +6 -6
  100. package/lib/oh-story/skills/story/VERSION +1 -1
  101. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
  102. package/lib/oh-story/skills/story/references/author-memory.md +40 -118
  103. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
  104. package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
  105. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
  106. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
  107. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  108. package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
  109. package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
  110. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
  111. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
  112. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
  113. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
  114. package/lib/oh-story/skills/story-import/SKILL.md +63 -579
  115. package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
  116. package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
  117. package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
  118. package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
  119. package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
  120. package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
  121. package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
  122. package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
  123. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
  124. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
  125. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
  126. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
  127. package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
  128. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
  129. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
  130. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
  131. package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
  132. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
  133. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
  134. package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
  135. package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
  136. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
  137. package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  138. package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  139. package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  140. package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  141. package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
  142. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
  143. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
  144. package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  145. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
  146. package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
  147. package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  148. package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
  149. package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  150. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
  151. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
  152. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
  153. package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
  154. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
  155. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
  156. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
  157. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  158. package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
  159. package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
  160. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
  161. package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
  162. package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
  163. package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
  164. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
  165. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  166. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
  167. package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
  168. package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
  169. package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
  170. package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
  171. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
  172. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
  173. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
  174. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
  175. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
  176. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
  177. package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
  178. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
  179. package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
  180. package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
  181. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
  182. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  183. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
  184. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
  185. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
  186. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
  187. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
  188. package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
  189. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
  190. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
  191. package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
  192. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
  193. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
  194. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
  195. package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  196. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
  197. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
  198. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
  199. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
  200. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
  201. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
  202. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
  203. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
  204. package/lib/oh-story/skills/story-review/SKILL.md +19 -273
  205. package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
  206. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
  207. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
  208. package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
  209. package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
  210. package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
  211. package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
  212. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
  213. package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
  214. package/lib/oh-story/skills/story-review/references/solo.md +52 -0
  215. package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
  216. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
  217. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
  218. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
  219. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
  220. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
  221. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
  222. package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
  223. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
  224. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
  225. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
  226. package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  227. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
  228. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
  229. package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  230. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
  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/long-genre-catalog.md +1 -1
  234. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
  236. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
  237. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
  238. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
  239. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
  240. package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
  241. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
  242. package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
  243. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
  244. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  245. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
  246. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
  247. package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
  248. package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
  249. package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
  250. package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
  251. package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
  252. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
  253. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
  254. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
  255. package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
  257. package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
  258. package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
  259. package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  260. package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
  261. package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
  262. package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
  263. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
  264. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  265. package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
  266. package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
  267. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
  268. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
  270. package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
  271. package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
  272. package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
  273. package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
  274. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
  276. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
  277. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
  278. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
  279. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
  280. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
  281. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
  282. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
  283. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
  284. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
  285. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
  286. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
  287. package/lib/types/client/video-studio.d.ts.map +1 -1
  288. package/lib/types/native-hooks.d.ts +2 -2
  289. package/lib/types/native-hooks.d.ts.map +1 -1
  290. package/lib/types/role-tool.d.ts.map +1 -1
  291. package/lib/types/skill-provider.d.ts.map +1 -1
  292. package/lib/types/video-project.d.ts.map +1 -1
  293. package/lib/types/workspace-route.d.ts.map +1 -1
  294. package/lib/video-recap/CHANGELOG.md +60 -1
  295. package/lib/video-recap/README.en.md +348 -137
  296. package/lib/video-recap/README.md +340 -136
  297. package/lib/video-recap/manifest.json +406 -251
  298. package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
  299. package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
  300. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  301. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
  302. package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
  303. package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
  304. package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
  305. package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
  306. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
  307. package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  308. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  309. package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  310. package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  311. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  312. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  313. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
  314. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
  315. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
  316. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
  317. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
  318. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
  319. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
  320. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
  321. package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  322. package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
  323. package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
  324. package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
  325. package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
  326. package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
  327. package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
  328. package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
  329. package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
  330. package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
  331. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
  332. package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
  333. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
  334. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
  335. package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
  336. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
  337. package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  338. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
  339. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
  340. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
  341. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  342. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
  343. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
  344. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
  345. package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
  346. package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
  347. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
  348. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
  349. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
  350. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
  351. package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
  352. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
  353. package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
  354. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
  355. package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
  356. package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
  357. package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
  358. package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
  359. package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
  360. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
  361. package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
  362. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
  363. package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
  364. package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
  365. package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
  366. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
  367. package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
  368. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
  369. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
  370. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
  371. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
  372. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
  373. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
  374. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
  375. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
  376. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
  377. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
  378. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
  379. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
  380. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
  381. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
  382. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
  383. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
  384. package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
  385. package/lib/video-recap/skills/video-script/SKILL.md +16 -13
  386. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
  387. package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
  388. package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
  389. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
  390. package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
  391. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
  392. package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
  393. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
  394. package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
  395. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
  396. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
  397. package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
  398. package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
  399. package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
  400. package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
  401. package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
  402. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  403. package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
  404. package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  405. package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
  406. package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
  407. package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
  408. package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
  409. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
  410. package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
  411. package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
  412. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
  413. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
  414. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
  415. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
  416. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
  417. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
  418. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
  419. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
  420. package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
  421. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
  422. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  423. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
  424. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
  425. package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  426. package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  427. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
  428. package/package.json +41 -41
  429. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
  430. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
  431. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
  432. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
  433. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
  434. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
  435. package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
  436. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
  437. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
  438. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
  439. package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
  440. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
  441. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
  442. /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
  443. /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
  444. /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
@@ -0,0 +1,123 @@
1
+ """Fail-closed approved narration policy and current-metadata lifecycle helpers."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+
10
+ LEGACY_TEXT_POLICY = "legacy-auto-truncate-v1"
11
+ PRESERVE_APPROVED_TEXT_POLICY = "preserve-approved-text-v1"
12
+
13
+
14
+ class ApprovedTextPolicyError(RuntimeError):
15
+ def __init__(self, message, evidence):
16
+ self.evidence = evidence
17
+ super().__init__(
18
+ f"{message}: " + json.dumps(evidence, ensure_ascii=False, sort_keys=True)
19
+ )
20
+
21
+
22
+ class ApprovedTextDurationError(ApprovedTextPolicyError):
23
+ """Approved narration cannot fit its authored window without changing text."""
24
+
25
+ def __init__(self, evidence):
26
+ super().__init__(
27
+ "批准旁白无法在不删改文本的前提下安全放入时间窗;请扩大窗口或由作者修订文本",
28
+ evidence,
29
+ )
30
+
31
+
32
+ def policy_name(preserve_approved_text):
33
+ if preserve_approved_text:
34
+ return PRESERVE_APPROVED_TEXT_POLICY
35
+ return LEGACY_TEXT_POLICY
36
+
37
+
38
+ def validate_required_texts(narration, clean_text, preserve_approved_text):
39
+ """Reject authored segments whose provider-cleaned spoken text is empty."""
40
+ if not preserve_approved_text:
41
+ return
42
+ for index, segment in enumerate(narration):
43
+ spoken_text = clean_text(segment["narration"])
44
+ if spoken_text:
45
+ continue
46
+ evidence = {
47
+ "failure_kind": "approved_text_empty_after_cleanup",
48
+ "policy": PRESERVE_APPROVED_TEXT_POLICY,
49
+ "required": True,
50
+ "index": index,
51
+ "segment": index + 1,
52
+ "authored_text": segment["narration"],
53
+ "spoken_text": spoken_text,
54
+ "window_start": segment["start"],
55
+ "window_end": segment["end"],
56
+ }
57
+ raise ApprovedTextPolicyError(
58
+ "批准稿必需段经显式格式/舞台提示清理后没有可合成文本;请修订或删除该作者段",
59
+ evidence,
60
+ )
61
+
62
+
63
+ def enforce_duration(index, segment, authored_text, spoken_text, audio_duration,
64
+ available_duration, max_raw_duration, preserve_approved_text,
65
+ default_pause_ms):
66
+ if not preserve_approved_text or audio_duration <= max_raw_duration:
67
+ return
68
+ evidence = {
69
+ "failure_kind": "approved_text_duration_conflict",
70
+ "policy": PRESERVE_APPROVED_TEXT_POLICY,
71
+ "index": index,
72
+ "segment": index + 1,
73
+ "authored_text": authored_text,
74
+ "spoken_text": spoken_text,
75
+ "audio_duration": round(audio_duration, 6),
76
+ "window_start": segment["start"],
77
+ "window_end": segment["end"],
78
+ "window_duration": round(segment["end"] - segment["start"], 6),
79
+ "pause_after_ms": segment.get("pause_after_ms", default_pause_ms),
80
+ "available_duration": round(available_duration, 6),
81
+ "max_raw_duration": round(max_raw_duration, 6),
82
+ }
83
+ raise ApprovedTextDurationError(evidence)
84
+
85
+
86
+ def archive_current_meta(meta_path):
87
+ """Atomically move a current meta file into timestamp-named history."""
88
+ path = Path(meta_path)
89
+ if not path.is_file():
90
+ return None
91
+ history_dir = path.parent / "tts_meta.history"
92
+ history_dir.mkdir(exist_ok=True)
93
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")
94
+ archived = history_dir / f"{stamp}.json"
95
+ counter = 0
96
+ while archived.exists():
97
+ counter += 1
98
+ archived = history_dir / f"{stamp}-{counter}.json"
99
+ os.replace(path, archived)
100
+ return archived
101
+
102
+
103
+ def write_json_atomically(path, payload):
104
+ """Replace JSON only after a complete same-directory write and fsync."""
105
+ destination = Path(path)
106
+ temp_path = None
107
+ try:
108
+ with tempfile.NamedTemporaryFile(
109
+ mode="w",
110
+ encoding="utf-8",
111
+ dir=destination.parent,
112
+ prefix=f".{destination.stem}.",
113
+ suffix=".tmp",
114
+ delete=False,
115
+ ) as handle:
116
+ temp_path = Path(handle.name)
117
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
118
+ handle.flush()
119
+ os.fsync(handle.fileno())
120
+ os.replace(temp_path, destination)
121
+ finally:
122
+ if temp_path is not None:
123
+ temp_path.unlink(missing_ok=True)
@@ -20,7 +20,6 @@ brittle and case-by-case. Two stages around an agent-authored pause:
20
20
  """
21
21
  import argparse
22
22
  import base64
23
- import hashlib
24
23
  import json
25
24
  import re
26
25
  import unicodedata
@@ -28,6 +27,7 @@ import wave
28
27
  from pathlib import Path
29
28
 
30
29
  from lib import (
30
+ file_identity,
31
31
  CONFIG,
32
32
  get_video_duration,
33
33
  log,
@@ -44,7 +44,6 @@ ATEMPO_CAP = 2.0 # max compression before we trim instead (atempo>2 also sounds
44
44
  DUB_FAST_SPEECH_CPS = 7.0
45
45
  DUB_TRIM_RISK_CPS = 7.0
46
46
  DUB_MIN_ASR_WINDOW_SECONDS = 0.5
47
- DUB_TTS_CACHE_VERSION = 1
48
47
  DUB_TTS_STYLE_PROMPT = "自然、清晰,保持原说话人的音色与节奏,语气平稳。"
49
48
 
50
49
  DUB_SCHEMA_VERSION = 1
@@ -208,32 +207,32 @@ def lint_dub_script(script, duration, work_dir=None):
208
207
 
209
208
  def build_dub_review(script, transcript, lint=None):
210
209
  """Script-level review scaffold: deterministic timing/naturalness signals for agent review."""
211
- duration = float(transcript.get("duration", 0.0))
210
+ duration = float(transcript["duration"])
212
211
  lint = lint or lint_dub_script(script, duration)
213
212
  lines = _normalize_dub_script(script) if isinstance(script, list) else []
214
213
  edits = []
215
- for issue in lint.get("issues", []):
214
+ for issue in lint["issues"]:
216
215
  if issue["severity"] in {"error", "warning"}:
217
216
  edits.append({
218
- "line": issue.get("line"),
219
- "start": issue.get("start"),
217
+ "line": issue["line"],
218
+ "start": issue.get("start"), # _issue only records start when the line had one
220
219
  "issue": f"{issue['code']}: {issue['message']}",
221
220
  "suggestion": "缩短译文、调整 start/end,或回到 transcript 核对原句边界。",
222
221
  })
223
- verdict = "FAIL" if lint.get("verdict") == "FAIL" else ("REVISE" if edits else "PASS")
222
+ verdict = "FAIL" if lint["verdict"] == "FAIL" else ("REVISE" if edits else "PASS")
224
223
  return {
225
224
  "schema_version": DUB_SCHEMA_VERSION,
226
225
  "verdict": verdict,
227
226
  "checks": {
228
227
  "faithful_to_source": "needs_agent_review",
229
- "spoken_chinese": "REVISE" if any(i.get("code") == "fast_speech" for i in lint.get("issues", [])) else "PASS",
228
+ "spoken_chinese": "REVISE" if any(i["code"] == "fast_speech" for i in lint["issues"]) else "PASS",
230
229
  "speaker_tone": "needs_agent_review",
231
- "timing_fit": "FAIL" if lint.get("verdict") == "FAIL" else ("REVISE" if edits else "PASS"),
230
+ "timing_fit": verdict,
232
231
  "platform_fit": "needs_agent_review",
233
232
  },
234
233
  "highest_return_edits": edits[:8],
235
234
  "notes": "Deterministic script-level review; an agent/human should fill semantic fidelity and tone judgments against dub_transcript.json.",
236
- "coverage": {"transcript_windows": len(transcript.get("windows", [])), "script_lines": len(lines)},
235
+ "coverage": {"transcript_windows": len(transcript["windows"]), "script_lines": len(lines)},
237
236
  }
238
237
 
239
238
 
@@ -244,12 +243,19 @@ def _write_json(path, payload):
244
243
 
245
244
  # ── ffmpeg / wav helpers ─────────────────────────────────────────────
246
245
 
246
+ def _ffmpeg(args):
247
+ """Run one ffmpeg command; a non-zero exit raises instead of leaving a silent gap."""
248
+ result = run_cmd(["ffmpeg", "-y", *args])
249
+ if result.returncode != 0:
250
+ raise RuntimeError(f"ffmpeg 失败 ({args[-1]}): {result.stderr.strip()[-2000:]}")
251
+
252
+
247
253
  def _ffmpeg_extract_wav(video, out_wav, sr=16000):
248
- run_cmd(["ffmpeg", "-y", "-i", str(video), "-vn", "-ar", str(sr), "-ac", "1", str(out_wav)])
254
+ _ffmpeg(["-i", str(video), "-vn", "-ar", str(sr), "-ac", "1", str(out_wav)])
249
255
 
250
256
 
251
257
  def _cut_wav(src_wav, out_wav, start, dur, sr=16000):
252
- run_cmd(["ffmpeg", "-y", "-i", str(src_wav), "-ss", str(start), "-t", str(dur),
258
+ _ffmpeg(["-i", str(src_wav), "-ss", str(start), "-t", str(dur),
253
259
  "-ar", str(sr), "-ac", "1", str(out_wav)])
254
260
 
255
261
 
@@ -285,18 +291,14 @@ def _strip_reasoning_residue(text):
285
291
 
286
292
 
287
293
  def _run_asr(wav_path, lang="en"):
288
- try:
289
- with wave.open(str(wav_path), "rb") as source:
290
- if source.getnframes() <= 0:
291
- return ""
292
- except (OSError, EOFError, wave.Error):
293
- return ""
294
- raw = Path(wav_path).read_bytes()
295
- if not raw:
296
- return ""
297
- b64 = base64.b64encode(raw).decode("ascii")
294
+ # The WAV was just cut by ffmpeg (which raised on failure); an unreadable file is a bug,
295
+ # not "no speech". A zero-frame window (audio shorter than the picture) is skipped.
296
+ with wave.open(str(wav_path), "rb") as source:
297
+ if source.getnframes() <= 0:
298
+ return ""
299
+ b64 = base64.b64encode(Path(wav_path).read_bytes()).decode("ascii")
298
300
  payload = {
299
- "model": CONFIG.get("mimo_asr_model", "mimo-v2.5-asr"),
301
+ "model": CONFIG["mimo_asr_model"],
300
302
  "messages": [{"role": "user", "content": [
301
303
  {"type": "input_audio", "input_audio": {"data": f"data:{ASR_MIME};base64,{b64}"}},
302
304
  ]}],
@@ -305,8 +307,8 @@ def _run_asr(wav_path, lang="en"):
305
307
  resp = mimo_asr_api_call(payload)
306
308
  try:
307
309
  text = str(resp["choices"][0]["message"]["content"] or "").strip()
308
- except (KeyError, IndexError, TypeError):
309
- return ""
310
+ except (KeyError, IndexError, TypeError) as exc:
311
+ raise RuntimeError("MiMo-ASR 响应缺少 choices[0].message.content") from exc
310
312
  return re.sub(r"<[^>]{1,20}>", "", _strip_reasoning_residue(text)).strip() # strip reasoning + ASR markers like "<chinese>"
311
313
 
312
314
 
@@ -346,16 +348,13 @@ def _clone_tts(text, ref_b64, out_wav):
346
348
  Path(out_wav).write_bytes(base64.b64decode(data))
347
349
 
348
350
 
349
- def _clone_cache_fingerprint(text, ref_bytes):
350
- payload = {
351
- "cache_version": DUB_TTS_CACHE_VERSION,
351
+ def _clone_cache_inputs(text, ref_identity):
352
+ return {
352
353
  "model": CLONE_MODEL,
353
354
  "style_prompt": DUB_TTS_STYLE_PROMPT,
354
355
  "text": str(text),
355
- "reference_sha256": hashlib.sha256(ref_bytes).hexdigest(),
356
+ "reference": ref_identity,
356
357
  }
357
- encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
358
- return hashlib.sha256(encoded).hexdigest()
359
358
 
360
359
 
361
360
  def _clone_cache_meta_path(raw_wav):
@@ -371,30 +370,27 @@ def _usable_clone_wav(path):
371
370
  return sample_rate > 0 and channels > 0 and bool(frames)
372
371
 
373
372
 
374
- def _write_clone_cache_meta(raw_wav, text, ref_bytes):
375
- meta = {
376
- "schema_version": DUB_TTS_CACHE_VERSION,
377
- "fingerprint": _clone_cache_fingerprint(text, ref_bytes),
378
- "model": CLONE_MODEL,
379
- }
373
+ def _write_clone_cache_meta(raw_wav, text, ref_identity):
374
+ meta = {"inputs": _clone_cache_inputs(text, ref_identity)}
380
375
  return _write_json(_clone_cache_meta_path(raw_wav), meta)
381
376
 
382
377
 
383
- def _ensure_clone_tts(text, ref_b64, ref_bytes, raw_wav):
378
+ def _ensure_clone_tts(text, ref_b64, ref_identity, raw_wav):
384
379
  raw_wav = Path(raw_wav)
385
- expected = _clone_cache_fingerprint(text, ref_bytes)
380
+ expected = _clone_cache_inputs(text, ref_identity)
386
381
  meta_path = _clone_cache_meta_path(raw_wav)
387
- try:
382
+ # Missing sidecar = never synthesized; a sidecar this skill wrote but cannot parse is a
383
+ # bug, not a reason to silently pay for re-synthesis.
384
+ if meta_path.exists():
388
385
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
389
- except (FileNotFoundError, OSError, ValueError, TypeError):
390
- meta = {}
391
- if meta.get("fingerprint") == expected and _usable_clone_wav(raw_wav):
392
- return True
386
+ # A meta from the content-hash schema has no "inputs" and is simply a miss.
387
+ if meta.get("inputs") == expected and _usable_clone_wav(raw_wav):
388
+ return True
393
389
 
394
390
  _clone_tts(text, ref_b64, raw_wav)
395
391
  if not _usable_clone_wav(raw_wav):
396
392
  raise RuntimeError(f"MiMo voiceclone returned an invalid WAV: {raw_wav}")
397
- _write_clone_cache_meta(raw_wav, text, ref_bytes)
393
+ _write_clone_cache_meta(raw_wav, text, ref_identity)
398
394
  return False
399
395
 
400
396
 
@@ -403,17 +399,15 @@ def _time_fit(raw_wav, fitted_wav, room_seconds):
403
399
  the next line). Never globally speed up; a short dub keeps the natural pause. Returns the
404
400
  fitted duration."""
405
401
  dur = get_video_duration(raw_wav)
406
- if dur <= 0:
407
- return 0.0
408
402
  if dur <= room_seconds + 0.05:
409
- run_cmd(["ffmpeg", "-y", "-i", str(raw_wav), "-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
403
+ _ffmpeg(["-i", str(raw_wav), "-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
410
404
  return dur
411
405
  factor = dur / room_seconds
412
406
  if factor <= ATEMPO_CAP:
413
- run_cmd(["ffmpeg", "-y", "-i", str(raw_wav), "-filter:a", _atempo_chain(factor),
407
+ _ffmpeg(["-i", str(raw_wav), "-filter:a", _atempo_chain(factor),
414
408
  "-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
415
409
  return get_video_duration(fitted_wav)
416
- run_cmd(["ffmpeg", "-y", "-i", str(raw_wav),
410
+ _ffmpeg(["-i", str(raw_wav),
417
411
  "-filter:a", f"{_atempo_chain(ATEMPO_CAP)},atrim=0:{room_seconds:.3f},"
418
412
  f"afade=t=out:st={max(0, room_seconds - 0.15):.3f}:d=0.15",
419
413
  "-ar", str(CLONE_SR), "-ac", "1", str(fitted_wav)])
@@ -423,12 +417,10 @@ def _time_fit(raw_wav, fitted_wav, room_seconds):
423
417
  def _build_dub_track(lines, duration, out_wav):
424
418
  canvas = bytearray(int(duration * CLONE_SR) * 2) # 16-bit mono silence
425
419
  for ln in lines:
426
- fw = ln.get("fitted_wav")
427
- if not fw or not Path(fw).exists():
428
- continue
429
- sr, ch, frames = _wav_frames(fw)
420
+ # _time_fit always writes fitted_wav at CLONE_SR mono (and raised if ffmpeg failed).
421
+ sr, ch, frames = _wav_frames(ln["fitted_wav"])
430
422
  if sr != CLONE_SR or ch != 1:
431
- continue
423
+ raise RuntimeError(f"fitted WAV {ln['fitted_wav']} is {sr}Hz/{ch}ch, expected {CLONE_SR}Hz mono")
432
424
  off = int(ln["start"] * CLONE_SR) * 2
433
425
  end = min(off + len(frames), len(canvas))
434
426
  canvas[off:end] = frames[: end - off]
@@ -440,7 +432,7 @@ def _build_dub_track(lines, duration, out_wav):
440
432
 
441
433
 
442
434
  def _mux(video, dub_wav, out_video):
443
- run_cmd(["ffmpeg", "-y", "-i", str(video), "-i", str(dub_wav),
435
+ _ffmpeg(["-i", str(video), "-i", str(dub_wav),
444
436
  "-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy",
445
437
  "-af", "loudnorm=I=-16:TP=-1.5:LRA=11", "-c:a", "aac", "-b:a", "192k",
446
438
  "-ar", str(DUB_DELIVERY_SR),
@@ -529,8 +521,8 @@ def stage_render(video, work, ref_start, ref_dur):
529
521
  if not ref_wav.exists():
530
522
  rs, rd = _ref_window(duration, ref_start, ref_dur)
531
523
  _cut_wav(work / "dub_source.wav", ref_wav, rs, rd)
532
- ref_bytes = ref_wav.read_bytes()
533
- ref_b64 = base64.b64encode(ref_bytes).decode("ascii")
524
+ ref_identity = file_identity(ref_wav)
525
+ ref_b64 = base64.b64encode(ref_wav.read_bytes()).decode("ascii")
534
526
 
535
527
  tts_dir = work / "dub_tts"
536
528
  tts_dir.mkdir(exist_ok=True)
@@ -542,7 +534,7 @@ def stage_render(video, work, ref_start, ref_dur):
542
534
  room = max(0.4, min(slot_end, nxt) - ln["start"])
543
535
  raw = tts_dir / f"line_{i:03d}_raw.wav"
544
536
  fitted = tts_dir / f"line_{i:03d}.wav"
545
- cache_hit = _ensure_clone_tts(ln["zh"], ref_b64, ref_bytes, raw)
537
+ cache_hit = _ensure_clone_tts(ln["zh"], ref_b64, ref_identity, raw)
546
538
  ln["tts_cache"] = "hit" if cache_hit else "miss"
547
539
  ln["fitted_wav"] = str(fitted)
548
540
  ln["fitted_dur"] = round(_time_fit(raw, fitted, room), 2)
@@ -1,7 +1,6 @@
1
1
  """Self-contained config + utilities for this skill (no cross-skill imports).
2
2
  Merged from the shared core; reads the same env vars as the rest of the bundle."""
3
3
  import json
4
- import hashlib
5
4
  import math
6
5
  import os
7
6
  import re
@@ -116,7 +115,7 @@ CONFIG = {
116
115
  "mimo_api_key": _mimo_api_key,
117
116
  "mimo_tts_api_url": normalize_api_url(_raw_mimo_tts_api_url),
118
117
  "mimo_tts_api_key": _mimo_tts_api_key,
119
- "mimo_tts_api_key_source": "MIMO_TTS_API_KEY" if os.environ.get("MIMO_TTS_API_KEY") else "MIMO_API_KEY",
118
+ "mimo_tts_env_var": "MIMO_TTS_API_KEY" if os.environ.get("MIMO_TTS_API_KEY") else "MIMO_API_KEY",
120
119
  "mimo_asr_model": os.environ.get("MIMO_ASR_MODEL", DEFAULT_MIMO_ASR_MODEL),
121
120
  "mimo_tts_model": os.environ.get("MIMO_TTS_MODEL", DEFAULT_MIMO_TTS_MODEL),
122
121
  "mimo_tts_voice": os.environ.get("MIMO_TTS_VOICE", "冰糖"),
@@ -190,45 +189,10 @@ def get_video_duration(video_path):
190
189
  return float(result.stdout.strip())
191
190
 
192
191
 
193
- def stable_hash(value):
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
-
198
-
199
- _FILE_FINGERPRINT_MEMO = {}
200
-
201
-
202
- def _file_identity(path):
203
- """(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
192
+ def file_identity(path):
193
+ """{size, mtime_ns} — the cache identity of an input file (no content read)."""
204
194
  st = os.stat(os.fspath(path))
205
- return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
206
-
207
-
208
- def file_fingerprint(path, chunk_size=1024 * 1024):
209
- """Return a full-content fingerprint for cache-correct identity checks.
210
-
211
- The digest covers CONTENT only — never the path or mtime — so a copied video or
212
- artifact is still recognised as the same asset, while any byte change invalidates
213
- the cache even if timestamps, size, head, or tail bytes are misleading.
214
-
215
- Identity metadata is used ONLY to memoize within a single process. One understanding
216
- run fingerprints the same source video 8-10 times and the whole extracted frame set
217
- 2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
218
- any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
219
- re-hashed, so the memo can never serve a stale digest.
220
- """
221
- key = _file_identity(path)
222
- memoized = _FILE_FINGERPRINT_MEMO.get(key)
223
- if memoized is not None:
224
- return memoized
225
- h = hashlib.sha256()
226
- with open(os.fspath(path), "rb") as f:
227
- for chunk in iter(lambda: f.read(chunk_size), b""):
228
- h.update(chunk)
229
- digest = h.hexdigest()
230
- _FILE_FINGERPRINT_MEMO[key] = digest
231
- return digest
195
+ return {"size": st.st_size, "mtime_ns": st.st_mtime_ns}
232
196
 
233
197
 
234
198
  def _retry_after_seconds(value, fallback):
@@ -288,7 +252,7 @@ def mimo_tts_api_call(payload):
288
252
  max_retries=10,
289
253
  api_url=CONFIG["mimo_tts_api_url"],
290
254
  api_key=CONFIG["mimo_tts_api_key"],
291
- api_key_source=CONFIG["mimo_tts_api_key_source"],
255
+ api_env_var=CONFIG["mimo_tts_env_var"],
292
256
  )
293
257
 
294
258
 
@@ -299,11 +263,11 @@ def mimo_asr_api_call(payload):
299
263
  max_retries=10,
300
264
  api_url=CONFIG["mimo_api_url"],
301
265
  api_key=CONFIG["mimo_api_key"],
302
- api_key_source="MIMO_API_KEY",
266
+ api_env_var="MIMO_API_KEY",
303
267
  )
304
268
 
305
269
 
306
- def api_call(payload, *, api_url, api_key, api_key_source, max_retries=8):
270
+ def api_call(payload, *, api_url, api_key, api_env_var, max_retries=8):
307
271
  """调用 OpenAI-compatible API,带重试。
308
272
 
309
273
  集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
@@ -330,7 +294,7 @@ def api_call(payload, *, api_url, api_key, api_key_source, max_retries=8):
330
294
  wait = _retry_after_seconds(retry_after, max(wait, 10))
331
295
  log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
332
296
  elif e.code == 401:
333
- raise RuntimeError(f"API 认证失败 (401)。请检查 {api_key_source} 和 API URL 是否匹配。")
297
+ raise RuntimeError(f"API 认证失败 (401)。请检查 {api_env_var} 和 API URL 是否匹配。")
334
298
  elif e.code == 403:
335
299
  hint = "API 访问被拒绝 (403)。"
336
300
  if "1010" in body or "cloudflare" in body.lower():
@@ -0,0 +1 @@
1
+ """TTS provider transports (fish-audio, index-tts) for the video-voiceover skill."""
@@ -0,0 +1,184 @@
1
+ """Self-hosted index-tts JSON-to-WAV transport with no redirect forwarding."""
2
+
3
+ import contextlib
4
+ import io
5
+ import json
6
+ import os
7
+ import socket
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ import wave
12
+ from pathlib import Path
13
+
14
+
15
+ MAX_WAV_BYTES = 64 * 1024 * 1024
16
+ MAX_ERROR_BYTES = 512
17
+ SPEED_POLICY = "provider-default-no-rate-control"
18
+
19
+
20
+ class _SafeResponseError(RuntimeError):
21
+ pass
22
+
23
+
24
+ class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
25
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
26
+ return None
27
+
28
+
29
+ def _open_without_redirects(request, timeout):
30
+ return urllib.request.build_opener(_NoRedirectHandler()).open(request, timeout=timeout)
31
+
32
+
33
+ def validate_index_tts_config(endpoint, voice):
34
+ """Validate explicit private settings without returning a credential-bearing URL."""
35
+ endpoint = str(endpoint or "")
36
+ voice = str(voice or "").strip()
37
+ if not endpoint:
38
+ raise ValueError("INDEX_TTS_ENDPOINT 必须显式配置")
39
+ if any(ord(char) < 32 or ord(char) == 127 for char in endpoint):
40
+ raise ValueError("INDEX_TTS_ENDPOINT 配置无效或包含禁止字符")
41
+ try:
42
+ parsed = urllib.parse.urlsplit(endpoint)
43
+ port = parsed.port
44
+ except ValueError:
45
+ raise ValueError("INDEX_TTS_ENDPOINT 配置无效") from None
46
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname:
47
+ raise ValueError("INDEX_TTS_ENDPOINT 必须是带主机名的 http/https URL")
48
+ if "@" in parsed.netloc or "?" in endpoint or "#" in endpoint:
49
+ raise ValueError("INDEX_TTS_ENDPOINT 禁止 userinfo、query 或 fragment 携带凭证")
50
+ if port is not None and not 1 <= port <= 65535:
51
+ raise ValueError("INDEX_TTS_ENDPOINT 配置无效")
52
+ if not voice:
53
+ raise ValueError("INDEX_TTS_VOICE 必须显式配置")
54
+ return endpoint, voice
55
+
56
+
57
+ def load_private_config(config, environ):
58
+ """Read the private index-tts settings; validate them once, only when that provider is selected."""
59
+ endpoint = environ.get("INDEX_TTS_ENDPOINT", "").strip()
60
+ voice = environ.get("INDEX_TTS_VOICE", "").strip()
61
+ if config["tts_provider"] == "index-tts":
62
+ endpoint, voice = validate_index_tts_config(endpoint, voice)
63
+ config["index_tts_endpoint"] = endpoint
64
+ config["index_tts_voice"] = voice
65
+
66
+
67
+ def cache_settings(config):
68
+ """Non-secret settings that decide reuse; the endpoint is never written to disk."""
69
+ return {
70
+ "index_tts_voice": config["index_tts_voice"],
71
+ "index_tts_speed_policy": SPEED_POLICY,
72
+ }
73
+
74
+
75
+ def default_controls(segment):
76
+ unsupported = []
77
+ for key, default in (("emotion", None), ("style", None), ("rate", "+0%"), ("pitch", "+0Hz")):
78
+ if segment.get(key) not in (None, "", default):
79
+ unsupported.append(key)
80
+ if unsupported:
81
+ raise RuntimeError(f"已配置的端点不接受段级控制字段: {', '.join(unsupported)}")
82
+ return "+0%", "+0Hz"
83
+
84
+
85
+ def synthesize_configured(text, output_path, config):
86
+ return synthesize_index_tts(
87
+ text, output_path, endpoint=config["index_tts_endpoint"],
88
+ voice=config["index_tts_voice"], timeout=config["tts_timeout"],
89
+ )
90
+
91
+
92
+ def valid_cached_receipt(cache_data, config):
93
+ """A cached index-tts segment must carry the receipt of a request for the current voice."""
94
+ receipt = cache_data.get("provider_receipt")
95
+ return isinstance(receipt, dict) and receipt == {
96
+ "provider": "index-tts",
97
+ "requested_voice": config["index_tts_voice"],
98
+ }
99
+
100
+
101
+ def _discard_http_error_body(error):
102
+ """Drain a bounded slice and close; the body is never surfaced (it may echo private URLs)."""
103
+ with contextlib.suppress(Exception):
104
+ try:
105
+ error.read(MAX_ERROR_BYTES)
106
+ finally:
107
+ error.close()
108
+
109
+
110
+ def _validate_wav(audio):
111
+ if len(audio) <= 44 or audio[:4] != b"RIFF" or audio[8:12] != b"WAVE":
112
+ raise RuntimeError("IndexTTS 返回的内容不是有效 WAV")
113
+ if int.from_bytes(audio[4:8], "little") + 8 > len(audio):
114
+ raise RuntimeError("IndexTTS 返回的 RIFF 声明长度超过响应,音频被截断")
115
+ try:
116
+ with wave.open(io.BytesIO(audio), "rb") as wav:
117
+ channels = wav.getnchannels()
118
+ sample_width = wav.getsampwidth()
119
+ sample_rate = wav.getframerate()
120
+ frames = wav.getnframes()
121
+ decoded = wav.readframes(frames)
122
+ except (EOFError, wave.Error) as exc:
123
+ raise RuntimeError("IndexTTS 返回的 WAV 结构损坏或不受支持") from exc
124
+ if not (1 <= channels <= 8 and 1 <= sample_width <= 4 and 8000 <= sample_rate <= 192000):
125
+ raise RuntimeError("IndexTTS 返回的 WAV 音频参数越界")
126
+ if frames <= 0:
127
+ raise RuntimeError("IndexTTS 返回的 WAV 没有音频帧")
128
+ if len(decoded) != frames * channels * sample_width:
129
+ raise RuntimeError("IndexTTS 返回的 WAV 音频数据被截断")
130
+
131
+
132
+ def synthesize_index_tts(text, output_path, *, endpoint, voice, timeout):
133
+ """POST the exact {voice,text} contract and atomically persist an authentic WAV.
134
+
135
+ endpoint/voice were validated by load_private_config; timeout by lib's env_int."""
136
+ body = json.dumps({"voice": voice, "text": text}, ensure_ascii=False).encode("utf-8")
137
+ request = urllib.request.Request(
138
+ endpoint,
139
+ data=body,
140
+ headers={
141
+ "Content-Type": "application/json",
142
+ "Accept": "audio/wav",
143
+ "User-Agent": "video-recap/1.0",
144
+ },
145
+ method="POST",
146
+ )
147
+ try:
148
+ with _open_without_redirects(request, timeout) as response:
149
+ content_type = response.headers.get("Content-Type", "").lower()
150
+ content_length = response.headers.get("Content-Length")
151
+ if content_length and int(content_length) > MAX_WAV_BYTES:
152
+ raise _SafeResponseError("IndexTTS 返回 WAV 过大,已拒绝读取")
153
+ audio = response.read(MAX_WAV_BYTES + 1)
154
+ except urllib.error.HTTPError as exc:
155
+ _discard_http_error_body(exc)
156
+ if 300 <= exc.code < 400:
157
+ raise RuntimeError("IndexTTS 禁止 HTTP 重定向,未向其他主机转发批准文本") from None
158
+ raise RuntimeError(f"IndexTTS 请求失败 (HTTP {exc.code}),远端返回错误状态") from None
159
+ except urllib.error.URLError as exc:
160
+ kind = "超时" if isinstance(exc.reason, (TimeoutError, socket.timeout)) else "网络"
161
+ raise RuntimeError(f"IndexTTS {kind}错误,未返回音频") from None
162
+ except (TimeoutError, socket.timeout):
163
+ raise RuntimeError("IndexTTS 超时错误,未返回音频") from None
164
+ except ValueError:
165
+ raise RuntimeError("IndexTTS 返回了无效的 Content-Length") from None
166
+ except _SafeResponseError as exc:
167
+ raise RuntimeError(str(exc)) from None
168
+ except Exception:
169
+ raise RuntimeError("IndexTTS 响应读取或关闭失败,未保存音频") from None
170
+
171
+ if len(audio) > MAX_WAV_BYTES:
172
+ raise RuntimeError("IndexTTS 返回 WAV 过大,已拒绝保存")
173
+ if "json" in content_type:
174
+ raise RuntimeError("IndexTTS 返回了 JSON 错误响应而不是音频;正文已隐藏")
175
+ _validate_wav(audio)
176
+
177
+ output = Path(output_path)
178
+ partial = Path(str(output) + ".part")
179
+ try:
180
+ partial.write_bytes(audio)
181
+ os.replace(partial, output)
182
+ finally:
183
+ partial.unlink(missing_ok=True)
184
+ return {"provider": "index-tts", "requested_voice": voice}