@oh-story/dsh 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.md +11 -11
  2. package/lib/client.js +35 -14
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +165 -69
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
  8. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
  11. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  12. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  21. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  22. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
  23. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
  24. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
  25. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
  26. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
  27. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
  29. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  30. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  31. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  32. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  33. package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
  34. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
  35. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
  36. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  37. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  38. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  39. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
  40. package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
  41. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
  42. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
  43. package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
  44. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  45. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
  46. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  47. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
  50. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
  51. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
  53. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
  54. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
  55. package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
  56. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  57. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  58. package/lib/index.js +4 -2
  59. package/lib/index.js.map +2 -2
  60. package/lib/novel-to-game/VERSION +1 -1
  61. package/lib/novel-to-game/manifest.json +28 -28
  62. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
  64. package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
  65. package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
  66. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
  67. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
  68. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
  69. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
  70. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
  71. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
  72. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
  73. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
  74. package/lib/oh-story/manifest.json +292 -172
  75. package/lib/oh-story/roles/character-designer.md +1 -1
  76. package/lib/oh-story/roles/narrative-writer.md +56 -242
  77. package/lib/oh-story/roles/story-architect.md +1 -1
  78. package/lib/oh-story/skills/story/SKILL.md +1 -1
  79. package/lib/oh-story/skills/story/VERSION +1 -1
  80. package/lib/oh-story/skills/story/references/author-memory.md +10 -8
  81. package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
  82. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
  83. package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
  84. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  85. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  86. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  87. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  88. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  89. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  90. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  91. package/lib/oh-story/skills/story-import/SKILL.md +2 -2
  92. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  93. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  94. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
  95. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
  96. package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
  97. package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
  98. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
  99. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
  100. package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
  101. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  102. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  103. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
  104. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
  105. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  106. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  107. package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
  108. package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
  109. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  110. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  111. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  112. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  113. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  114. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
  115. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
  116. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
  117. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
  118. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  119. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
  120. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
  121. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  122. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  123. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
  124. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  125. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  126. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
  127. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  128. package/lib/oh-story/skills/story-review/SKILL.md +13 -3
  129. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
  130. package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
  131. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  132. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
  133. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
  134. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  135. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  136. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
  137. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  138. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  139. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  140. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  141. package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
  142. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
  143. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  144. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  145. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
  146. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  147. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  148. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  149. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  150. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  151. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
  152. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  153. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  154. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  155. package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
  156. package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
  157. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  158. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  159. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
  160. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
  161. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  162. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  163. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
  164. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  165. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  166. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
  167. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  168. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  169. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  170. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  171. package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
  172. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  173. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  174. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  175. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  176. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  177. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
  178. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  179. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  180. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  181. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  182. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  183. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  184. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  185. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  186. package/lib/types/client/index.d.ts.map +1 -1
  187. package/lib/types/client/production-prompts.d.ts.map +1 -1
  188. package/lib/types/client/production-runtime.d.ts +17 -0
  189. package/lib/types/client/production-runtime.d.ts.map +1 -1
  190. package/lib/types/skill-provider.d.ts.map +1 -1
  191. package/lib/video-recap/CHANGELOG.md +31 -1
  192. package/lib/video-recap/manifest.json +162 -162
  193. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  194. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  195. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  196. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  197. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  198. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  199. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  200. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  201. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  202. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  203. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  204. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  205. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  206. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  207. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  208. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  209. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  210. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  211. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  212. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  213. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  214. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  215. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  216. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  217. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  218. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  219. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  220. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  221. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  222. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  223. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  224. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  225. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  226. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  227. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  228. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  229. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  230. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  231. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  232. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  233. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  234. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  235. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  236. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  237. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  238. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  239. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  240. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  241. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  242. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  243. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  244. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  245. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  246. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  247. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  248. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  249. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  250. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  251. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  252. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  253. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  254. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  255. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  256. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  257. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  258. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  259. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  260. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  262. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  263. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  264. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  265. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  266. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  267. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  268. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  269. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  270. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  271. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  272. package/package.json +41 -41
@@ -1,7 +1,6 @@
1
1
  """Load measured source-speech evidence and classify narration ownership."""
2
2
 
3
3
  import json
4
- import math
5
4
  from pathlib import Path
6
5
 
7
6
  from lib import CONFIG, stable_hash
@@ -17,36 +16,16 @@ def _empty_evidence(mode):
17
16
 
18
17
 
19
18
  def _read_json(path):
20
- try:
21
- return json.loads(Path(path).read_text(encoding="utf-8"))
22
- except (OSError, json.JSONDecodeError):
23
- return None
24
-
25
-
26
- def _timed_rows(payload, key):
27
- rows = payload.get(key, []) if isinstance(payload, dict) else []
28
- out = []
29
- for row in rows if isinstance(rows, list) else []:
30
- if not isinstance(row, dict):
31
- continue
32
- try:
33
- start, end = float(row.get("start")), float(row.get("end"))
34
- except (TypeError, ValueError):
35
- continue
36
- if math.isfinite(start) and math.isfinite(end) and end > start:
37
- out.append({**row, "start": start, "end": end})
38
- return out
19
+ """JSON artifact, or None when the optional file was never written."""
20
+ return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None
39
21
 
40
22
 
41
23
  def _output_payload_is_current(payload, work_dir):
42
- if not isinstance(payload, dict):
43
- return False
44
- if payload.get("schema_version") != 2 or payload.get("timeline") != "cut_output":
45
- return False
46
24
  plan = _read_json(Path(work_dir) / "clip_plan_validated.json")
47
- return bool(
48
- isinstance(plan, dict)
49
- and payload.get("clip_plan_fingerprint") == stable_hash(plan)
25
+ return (
26
+ payload is not None
27
+ and plan is not None
28
+ and payload["clip_plan_fingerprint"] == stable_hash(plan)
50
29
  )
51
30
 
52
31
 
@@ -55,55 +34,31 @@ def load_source_sentence_evidence(work_dir, mode="full"):
55
34
  if work_dir is None:
56
35
  return _empty_evidence(mode)
57
36
  work_dir = Path(work_dir)
58
- output_mode = mode in {"cut", "cut_output"}
59
- path = work_dir / (
60
- "speech_boundary_anchors_output.json"
61
- if output_mode
62
- else "speech_boundary_anchors.json"
63
- )
64
- payload = _read_json(path)
65
- if mode == "cut_output" and not _output_payload_is_current(payload, work_dir):
66
- return _empty_evidence(mode)
67
- if not isinstance(payload, dict):
68
- payload = {}
69
-
70
- speech_spans = _timed_rows(payload, "speech_spans")
71
- quiet_windows = _timed_rows(payload, "quiet_windows")
72
- if not output_mode:
73
- for name in ("asr_result.json", "asr_clean.json"):
74
- raw = _read_json(work_dir / name)
75
- if isinstance(raw, list):
76
- candidate = {"speech_spans": raw}
77
- elif isinstance(raw, dict):
78
- candidate = {"speech_spans": raw.get("segments", [])}
79
- else:
80
- continue
81
- speech_spans = _timed_rows(candidate, "speech_spans")
82
- if speech_spans:
83
- break
84
- raw_quiet = _read_json(work_dir / "silence_periods.json")
85
- candidate = {
86
- "quiet_windows": [
87
- row
88
- for row in raw_quiet
89
- if isinstance(row, dict) and not bool(row.get("has_speech", False))
90
- ]
91
- } if isinstance(raw_quiet, list) else {}
92
- quiet_windows = _timed_rows(candidate, "quiet_windows")
93
-
94
- anchors = []
95
- for anchor in payload.get("sentence_anchors", []):
96
- if not isinstance(anchor, dict) or anchor.get("confidence") not in {
97
- "high",
98
- "medium",
99
- }:
100
- continue
101
- try:
102
- when = float(anchor.get("time"))
103
- except (TypeError, ValueError):
104
- continue
105
- if math.isfinite(when) and when >= 0:
106
- anchors.append({**anchor, "time": round(when, 3)})
37
+ if mode == "full":
38
+ payload = _read_json(work_dir / "speech_boundary_anchors.json") or {
39
+ "sentence_anchors": []
40
+ }
41
+ speech_spans = [
42
+ row for row in _read_json(work_dir / "asr_result.json") or [] if row["text"]
43
+ ]
44
+ quiet_windows = [
45
+ row
46
+ for row in _read_json(work_dir / "silence_periods.json") or []
47
+ if not row["has_speech"]
48
+ ]
49
+ else:
50
+ payload = _read_json(work_dir / "speech_boundary_anchors_output.json")
51
+ if mode == "cut_output" and not _output_payload_is_current(payload, work_dir):
52
+ return _empty_evidence(mode)
53
+ if payload is None:
54
+ payload = {"sentence_anchors": [], "speech_spans": [], "quiet_windows": []}
55
+ speech_spans = payload["speech_spans"]
56
+ quiet_windows = payload["quiet_windows"]
57
+ anchors = [
58
+ anchor
59
+ for anchor in payload["sentence_anchors"]
60
+ if anchor["confidence"] in {"high", "medium"}
61
+ ]
107
62
  return {
108
63
  "anchors": sorted(anchors, key=lambda item: item["time"]),
109
64
  "speech_spans": speech_spans,
@@ -147,16 +102,9 @@ def _speech_overlap_excluding_quiet(start, end, speech, quiet):
147
102
 
148
103
  def segment_overlaps_source_speech(seg, evidence):
149
104
  """Classify aggregate mix ownership across the complete narration interval."""
150
- try:
151
- start, end = float(seg.get("start")), float(seg.get("end"))
152
- except (AttributeError, TypeError, ValueError):
153
- return bool(getattr(seg, "get", lambda *_: True)("overlaps_speech", True))
154
- duration = max(0.0, end - start)
105
+ start, end = seg["start"], seg["end"]
155
106
  quiet = evidence["quiet_windows"]
156
- quiet_min = max(
157
- 0.3,
158
- duration * float(CONFIG.get("quiet_overlap_min_ratio", 0.8) or 0.8),
159
- )
107
+ quiet_min = max(0.3, (end - start) * CONFIG["quiet_overlap_min_ratio"])
160
108
  speech = evidence["speech_spans"]
161
109
  if speech:
162
110
  return _speech_overlap_excluding_quiet(start, end, speech, quiet) > 0.05
@@ -167,12 +115,8 @@ def segment_overlaps_source_speech(seg, evidence):
167
115
  return bool(seg.get("overlaps_speech", True))
168
116
 
169
117
 
170
- def entry_overlaps_source_speech(seg, evidence, tolerance=0.05):
118
+ def entry_overlaps_source_speech(start, evidence, *, authored_overlap=True, tolerance=0.05):
171
119
  """Classify the entry instant; later quiet time cannot erase an unsafe start."""
172
- try:
173
- start = float(seg.get("start"))
174
- except (AttributeError, TypeError, ValueError):
175
- return True
176
120
  if any(
177
121
  row["start"] - tolerance <= start <= row["end"] + tolerance
178
122
  for row in evidence["quiet_windows"]
@@ -187,18 +131,13 @@ def entry_overlaps_source_speech(seg, evidence, tolerance=0.05):
187
131
  return False
188
132
  if evidence["anchors"] or evidence["require_measured"]:
189
133
  return True
190
- return bool(seg.get("overlaps_speech", True))
134
+ return bool(authored_overlap)
191
135
 
192
136
 
193
137
  def measure_narration_speech_ownership(narration, work_dir, mode="full"):
194
138
  """Return narration copies with aggregate ownership derived from evidence."""
195
139
  evidence = load_source_sentence_evidence(work_dir, mode=mode)
196
- measured = []
197
- for seg in narration if isinstance(narration, list) else []:
198
- if not isinstance(seg, dict):
199
- measured.append(seg)
200
- continue
201
- item = dict(seg)
202
- item["overlaps_speech"] = segment_overlaps_source_speech(item, evidence)
203
- measured.append(item)
204
- return measured
140
+ return [
141
+ {**seg, "overlaps_speech": segment_overlaps_source_speech(seg, evidence)}
142
+ for seg in narration
143
+ ]
@@ -1,88 +1,49 @@
1
1
  """Fuse scenes, ASR, and quiet windows for narration planning."""
2
2
 
3
- import importlib.util
4
-
5
-
6
- from pathlib import Path
7
-
8
3
  from lib import CONFIG
9
-
10
-
11
4
  from agent_text import _overlap_seconds, _recommended_char_budget
12
5
  from narration_lint import _validate_narration_budget
13
6
 
14
- try:
15
- from deslop_qc import analyze_deslop_qc
16
- except ModuleNotFoundError:
17
- _deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
18
- _deslop_qc_spec = importlib.util.spec_from_file_location(
19
- "deslop_qc", _deslop_qc_path
20
- )
21
- if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
22
- raise
23
- _deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
24
- _deslop_qc_spec.loader.exec_module(_deslop_qc_module)
25
- analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
7
+
8
+ def _quiet_windows(silence_periods):
9
+ return [qp for qp in silence_periods if not qp["has_speech"]]
26
10
 
27
11
 
28
12
  def _align_narration_to_quiet(narration, scenes_analysis, silence_periods):
29
13
  """Recompute overlaps_speech from real quiet windows; keep the agent's timing.
30
14
 
31
15
  The dense continuous-bed design places narration ON the pictured beat over a
32
- ducked original bed, so we no longer relocate segments into silence gaps. The
33
- old shift moved voiceover up to 3s off the moment it was written for and could
34
- silently blank a squeezed segment; both fought the design intent. We now only
35
- correct the overlaps_speech flag that the ducking stage consumes, leaving the
36
- agent's start/end (and text) intact.
37
- """
38
- if not silence_periods:
39
- for n in narration:
40
- n["overlaps_speech"] = True
41
- return _validate_narration_budget(narration, scenes_analysis)
16
+ ducked original bed, so segments are never relocated into silence gaps. Only
17
+ the overlaps_speech flag that the ducking stage consumes is corrected, leaving
18
+ the agent's start/end (and text) intact.
42
19
 
43
- quiet_windows = [
44
- qp for qp in silence_periods if _silence_window_is_usable_quiet(qp)
45
- ]
46
- quiet_ratio_min = float(CONFIG.get("quiet_overlap_min_ratio", 0.8) or 0.8)
47
-
48
- # Run budget/dedup FIRST, then set the flag on the final (possibly merged) spans,
49
- # so a dedup-merged beat's overlaps_speech reflects its extended timing, not its
50
- # original shorter span (which the ducking stage in assemble.py would mis-duck).
20
+ Budget/dedup runs FIRST so a dedup-merged beat's overlaps_speech reflects its
21
+ extended timing, not its original shorter span.
22
+ """
51
23
  aligned = _validate_narration_budget(narration, scenes_analysis)
24
+ quiet_windows = _quiet_windows(silence_periods)
25
+ quiet_ratio_min = CONFIG["quiet_overlap_min_ratio"]
52
26
  for n in aligned:
53
- try:
54
- seg_start = float(n["start"])
55
- seg_end = float(n["end"])
56
- except (KeyError, TypeError, ValueError):
57
- n["overlaps_speech"] = True
58
- continue
59
- seg_dur = max(0.0, seg_end - seg_start)
60
- quiet_overlap = _quiet_overlap_seconds(seg_start, seg_end, quiet_windows)
27
+ seg_dur = n["end"] - n["start"]
28
+ quiet_overlap = sum(
29
+ _overlap_seconds(n["start"], n["end"], qw["start"], qw["end"])
30
+ for qw in quiet_windows
31
+ )
61
32
  n["overlaps_speech"] = quiet_overlap < max(0.3, seg_dur * quiet_ratio_min)
62
-
63
33
  return aligned
64
34
 
65
35
 
66
36
  def _scene_asr_lines(asr_result, scene):
67
37
  lines = []
68
- for seg in asr_result or []:
69
- try:
70
- start = float(seg.get("start", 0))
71
- end = float(seg.get("end", start))
72
- except (TypeError, ValueError):
73
- continue
74
- if scene["start"] < end and scene["end"] > start:
75
- text = str(seg.get("text", "")).strip()
76
- if text:
77
- lines.append(f" [{start:.1f}-{end:.1f}] {text}")
38
+ for seg in asr_result:
39
+ if scene["start"] < seg["end"] and scene["end"] > seg["start"] and seg["text"]:
40
+ lines.append(f" [{seg['start']:.1f}-{seg['end']:.1f}] {seg['text']}")
78
41
  return lines
79
42
 
80
43
 
81
44
  def _quiet_windows_for_scene(silence_periods, scene):
82
45
  windows = []
83
- for qp in silence_periods or []:
84
- if not _silence_window_is_usable_quiet(qp):
85
- continue
46
+ for qp in _quiet_windows(silence_periods):
86
47
  if qp["start"] < scene["end"] and qp["end"] > scene["start"]:
87
48
  start = max(qp["start"], scene["start"])
88
49
  end = min(qp["end"], scene["end"])
@@ -91,87 +52,35 @@ def _quiet_windows_for_scene(silence_periods, scene):
91
52
  return windows
92
53
 
93
54
 
94
- def _silence_window_is_usable_quiet(qp):
95
- if not isinstance(qp, dict):
96
- return False
97
- reason = str(qp.get("has_speech_reason", "")).lower()
98
- granularity = str(qp.get("asr_granularity", "")).lower()
99
- if reason in {
100
- "coarse_asr_overlap_ignored",
101
- "asr_overlap_low_confidence_quiet",
102
- "coarse_asr_no_overlap",
103
- }:
104
- return True
105
- if granularity == "coarse_grid" and qp.get("has_speech") is True:
106
- return True
107
- return not qp.get("has_speech", False)
108
-
109
-
110
- def _quiet_overlap_seconds(start, end, quiet_windows):
111
- overlap_seconds = 0.0
112
- for qw in quiet_windows:
113
- try:
114
- if isinstance(qw, dict):
115
- q_start = float(qw.get("start", 0))
116
- q_end = float(qw.get("end", q_start))
117
- else:
118
- q_start, q_end = qw
119
- q_start = float(q_start)
120
- q_end = float(q_end)
121
- except (TypeError, ValueError):
122
- continue
123
- overlap_seconds += _overlap_seconds(start, end, q_start, q_end)
124
- return overlap_seconds
125
-
126
-
127
55
  def _build_timeline_fusion(scenes, asr_segments, silence_periods):
128
56
  """Fuse VLM scenes, ASR dialogue and quiet narration slots on one timeline."""
129
57
  fusion = []
130
- quiet_windows = [
131
- w for w in silence_periods or [] if _silence_window_is_usable_quiet(w)
132
- ]
133
- for scene in scenes or []:
134
- try:
135
- start = float(scene.get("start", 0))
136
- end = float(scene.get("end", start))
137
- except (TypeError, ValueError):
138
- continue
58
+ quiet_windows = _quiet_windows(silence_periods)
59
+ for scene in scenes:
60
+ start, end = scene["start"], scene["end"]
139
61
  dialogue_segments = []
140
62
  dialogue_overlap = 0.0
141
- for seg in asr_segments or []:
142
- if not isinstance(seg, dict):
143
- continue
144
- try:
145
- seg_start = float(seg.get("start", 0))
146
- seg_end = float(seg.get("end", seg_start))
147
- except (TypeError, ValueError):
148
- continue
149
- overlap = _overlap_seconds(start, end, seg_start, seg_end)
63
+ for seg in asr_segments:
64
+ overlap = _overlap_seconds(start, end, seg["start"], seg["end"])
150
65
  if overlap <= 0:
151
66
  continue
152
- text = str(seg.get("text", "")).strip()
153
67
  dialogue_overlap += overlap
154
68
  dialogue_segments.append(
155
69
  {
156
- "start": round(seg_start, 2),
157
- "end": round(seg_end, 2),
70
+ "start": round(seg["start"], 2),
71
+ "end": round(seg["end"], 2),
158
72
  "overlap_seconds": round(overlap, 2),
159
- "text": text,
73
+ "text": seg["text"],
160
74
  }
161
75
  )
162
76
 
163
77
  narration_slots = []
164
78
  for window in quiet_windows:
165
- try:
166
- w_start = float(window.get("start", 0))
167
- w_end = float(window.get("end", w_start))
168
- except (TypeError, ValueError):
169
- continue
170
- overlap = _overlap_seconds(start, end, w_start, w_end)
79
+ overlap = _overlap_seconds(start, end, window["start"], window["end"])
171
80
  if overlap <= 0:
172
81
  continue
173
- slot_start = max(start, w_start)
174
- slot_end = min(end, w_end)
82
+ slot_start = max(start, window["start"])
83
+ slot_end = min(end, window["end"])
175
84
  narration_slots.append(
176
85
  {
177
86
  "start": round(slot_start, 2),
@@ -183,9 +92,9 @@ def _build_timeline_fusion(scenes, asr_segments, silence_periods):
183
92
 
184
93
  fusion.append(
185
94
  {
186
- "scene_id": scene.get("scene_id"),
95
+ "scene_id": scene["scene_id"],
187
96
  "time_range": [round(start, 2), round(end, 2)],
188
- "visual_description": scene.get("description", ""),
97
+ "visual_description": scene["description"],
189
98
  "depth_analysis": scene.get("depth_analysis", ""),
190
99
  "frame_facts": scene.get("frame_facts", {}),
191
100
  "dialogue_segments": dialogue_segments,
@@ -24,10 +24,7 @@ def _parse_vlm_depth_response(raw_text):
24
24
 
25
25
  # 提取【描述】
26
26
  desc_match = re.search(r'【描述】\s*\n?(.*?)(?=【帧标签】|【深层分析】|$)', raw_text, re.DOTALL)
27
- if desc_match:
28
- description = desc_match.group(1).strip()
29
- else:
30
- description = raw_text.strip()
27
+ description = desc_match.group(1).strip() if desc_match else raw_text.strip()
31
28
 
32
29
  # 提取【帧标签】
33
30
  frame_facts = {}
@@ -89,13 +89,13 @@ DUB_ARTIFACT_SCHEMAS = {
89
89
 
90
90
 
91
91
  def _chars_per_second(text, room):
92
- room = max(float(room or 0.0), 0.001)
92
+ room = max(room, 0.001)
93
93
  effective = 0
94
- for ch in str(text or ""):
94
+ for ch in text:
95
95
  if ch.isspace():
96
96
  continue
97
97
  cat = unicodedata.category(ch)
98
- if cat.startswith("P") or cat.startswith("S"):
98
+ if cat.startswith(("P", "S")):
99
99
  continue
100
100
  effective += 1
101
101
  return effective / room
@@ -104,15 +104,13 @@ def _chars_per_second(text, room):
104
104
  def _issue(severity, code, line, message, start=None, end=None):
105
105
  item = {"severity": severity, "code": code, "line": line, "message": message}
106
106
  if start is not None:
107
- item["start"] = round(float(start), 3)
107
+ item["start"] = round(start, 3)
108
108
  if end is not None:
109
- item["end"] = round(float(end), 3)
109
+ item["end"] = round(end, 3)
110
110
  return item
111
111
 
112
112
 
113
113
  def _normalize_dub_script(script):
114
- if not isinstance(script, list):
115
- raise ValueError("dub_script.json must be a list")
116
114
  lines = []
117
115
  for i, item in enumerate(script):
118
116
  if not isinstance(item, dict):
@@ -212,10 +210,7 @@ def build_dub_review(script, transcript, lint=None):
212
210
  """Script-level review scaffold: deterministic timing/naturalness signals for agent review."""
213
211
  duration = float(transcript.get("duration", 0.0))
214
212
  lint = lint or lint_dub_script(script, duration)
215
- try:
216
- lines = _normalize_dub_script(script)
217
- except ValueError:
218
- lines = [] # non-list script already FAILs lint; review is just a scaffold here
213
+ lines = _normalize_dub_script(script) if isinstance(script, list) else []
219
214
  edits = []
220
215
  for issue in lint.get("issues", []):
221
216
  if issue["severity"] in {"error", "warning"}:
@@ -286,8 +281,7 @@ def _strip_reasoning_residue(text):
286
281
  """
287
282
  text = re.sub(r"(?is)<think\b.*?</think\s*>", "", text) # full <think>…</think> block
288
283
  text = re.sub(r"(?is)<think\b.*\Z", "", text) # unclosed/truncated <think tail
289
- text = re.sub(r"(?i)^\s*<?/?think\s*>\s*", "", text) # leading orphan/residual think tag
290
- return text
284
+ return re.sub(r"(?i)^\s*<?/?think\s*>\s*", "", text) # leading orphan/residual think tag
291
285
 
292
286
 
293
287
  def _run_asr(wav_path, lang="en"):
@@ -10,62 +10,50 @@ import urllib.error
10
10
  import urllib.request
11
11
  from pathlib import Path
12
12
 
13
- from lib import (
14
- CONFIG,
15
- DEFAULT_FISH_TTS_API_URL,
16
- DEFAULT_FISH_TTS_MODEL,
17
- _sanitize_api_error,
18
- )
13
+ from lib import CONFIG, _sanitize_api_error
19
14
 
20
15
 
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
16
+ def _fish_payload(text, rate):
26
17
  payload = {
27
18
  "text": text,
28
19
  "format": "wav",
29
20
  "normalize": True,
30
21
  "prosody": {
31
- "speed": max(0.5, min(2.0, speed)),
22
+ "speed": 1.0 + float(rate.rstrip("%")) / 100.0,
32
23
  "volume": 0,
33
24
  "normalize_loudness": True,
34
25
  },
35
26
  }
36
- reference_id = str(CONFIG.get("fish_tts_reference_id") or "").strip()
27
+ reference_id = CONFIG["fish_tts_reference_id"]
37
28
  if reference_id:
38
29
  payload["reference_id"] = reference_id
39
30
  return payload
40
31
 
41
32
 
42
- def synthesize_fish_audio(text, output_path, rate="+0%", **_ignored):
33
+ def synthesize_fish_audio(text, output_path, rate="+0%"):
43
34
  """Synthesize one narration block and atomically write the WAV response."""
44
- api_key = str(CONFIG.get("fish_api_key") or "").strip()
35
+ api_key = CONFIG["fish_api_key"]
45
36
  if not api_key:
46
37
  raise RuntimeError("请设置 FISH_API_KEY 用于 Fish Audio TTS")
47
38
 
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
39
  body = json.dumps(_fish_payload(text, rate), ensure_ascii=False).encode("utf-8")
51
40
  request = urllib.request.Request(
52
- endpoint,
41
+ CONFIG["fish_tts_api_url"],
53
42
  data=body,
54
43
  headers={
55
44
  "Authorization": f"Bearer {api_key}",
56
45
  "Content-Type": "application/json",
57
46
  "Accept": "audio/wav",
58
- "model": model,
47
+ "model": CONFIG["fish_tts_model"],
59
48
  "User-Agent": "video-recap/1.0",
60
49
  },
61
50
  method="POST",
62
51
  )
63
52
 
64
- timeout = max(1, int(CONFIG.get("tts_timeout", 300) or 300))
65
53
  try:
66
- with urllib.request.urlopen(request, timeout=timeout) as response:
54
+ with urllib.request.urlopen(request, timeout=CONFIG["tts_timeout"]) as response:
67
55
  audio = response.read()
68
- content_type = str(response.headers.get("Content-Type") or "").lower()
56
+ content_type = response.headers.get("Content-Type", "").lower()
69
57
  except urllib.error.HTTPError as exc:
70
58
  detail = _sanitize_api_error(
71
59
  exc.read().decode("utf-8", errors="replace"),
@@ -99,11 +87,5 @@ def synthesize_fish_audio(text, output_path, rate="+0%", **_ignored):
99
87
 
100
88
  output = Path(output_path)
101
89
  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
90
+ partial.write_bytes(audio)
91
+ os.replace(partial, output)