@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
@@ -4,7 +4,6 @@ import os
4
4
  import wave
5
5
  from pathlib import Path
6
6
 
7
- from assemble_constants import SEGMENT_AUDIO_SCHEMA_VERSION
8
7
  from lib import CONFIG, get_video_duration, log, narration_tempo_budget, run_cmd
9
8
 
10
9
  def _apply_narration_speed(
@@ -21,44 +20,30 @@ def _apply_narration_speed(
21
20
  snappier without the chipmunk effect. Rewrites each segment's audio_path/duration
22
21
  to the sped copy so the rest of assembly is unchanged. No-op at speed 1.0.
23
22
  """
24
- speed = float(CONFIG.get("narration_speed", 1.0) or 1.0)
23
+ speed = CONFIG["narration_speed"]
25
24
  if abs(speed - 1.0) <= 1e-3:
26
25
  return
27
- factor = max(0.5, min(2.0, speed))
28
26
  done = 0
29
27
  for seg in tts_segments:
30
- src = seg.get("audio_path")
31
- if not src or not os.path.exists(src):
32
- continue
33
- seg.setdefault("segment_audio_schema_version", SEGMENT_AUDIO_SCHEMA_VERSION)
34
- seg.setdefault("narration", str(seg.get("narration") or seg.get("spoken_text") or ""))
35
- seg.setdefault("spoken_text", str(seg.get("spoken_text") or seg.get("narration") or ""))
36
- seg.setdefault("truncated", False)
37
- seg.setdefault("truncate_reason", "none")
38
- seg.setdefault("source_audio_duration", seg.get("audio_duration"))
39
- seg["global_narration_speed"] = factor
40
- out = str(Path(work_dir) / f"_spd_{seg.get('index', 0)}.wav")
41
- res = command_runner(["ffmpeg", "-y", "-i", src, "-filter:a", f"atempo={factor:.3f}",
28
+ src = seg["audio_path"]
29
+ if not os.path.exists(src):
30
+ continue # reported as a skipped segment during placement
31
+ out = str(Path(work_dir) / f"_spd_{seg['index']}.wav")
32
+ res = command_runner(["ffmpeg", "-y", "-i", src, "-filter:a", f"atempo={speed:.3f}",
42
33
  "-ar", "44100", "-ac", "1", "-acodec", "pcm_s16le", out])
43
- if res.returncode == 0 and os.path.exists(out):
44
- seg["audio_path"] = out
45
- seg["audio_duration"] = duration_probe(out)
46
- seg["effective_tempo"] = (
47
- factor
48
- * (1.0 + float(seg.get("tts_rate_offset", 0.0) or 0.0))
49
- * float(seg.get("segment_tempo_factor", 1.0) or 1.0)
50
- )
51
- done += 1
52
- logger(f"解说整体提速: atempo={factor:.2f} ({done} 段)")
34
+ if res.returncode != 0:
35
+ raise RuntimeError(f"解说提速失败 {src}: {res.stderr}")
36
+ seg["audio_path"] = out
37
+ seg["audio_duration"] = duration_probe(out)
38
+ done += 1
39
+ logger(f"解说整体提速: atempo={speed:.2f} ({done} 段)")
53
40
 
54
41
 
55
42
  def _adjust_tts_speed(
56
43
  audio_path,
57
44
  target_duration,
58
- work_dir,
59
45
  tts_rate_offset=0.0,
60
46
  *,
61
- return_meta=True,
62
47
  command_runner=run_cmd,
63
48
  duration_probe=get_video_duration,
64
49
  logger=log,
@@ -69,7 +54,6 @@ def _adjust_tts_speed(
69
54
  audio fit, it returns `fit_status=no_safe_fit` and leaves the original audio
70
55
  untouched for QC to block instead of guessing a spoken_text truncation.
71
56
  """
72
- del work_dir, return_meta # Retained for direct callers of the pre-v0.4 private helper.
73
57
  audio_path = Path(audio_path)
74
58
  current_dur = duration_probe(audio_path)
75
59
  budget = narration_tempo_budget(tts_rate_offset)
@@ -80,7 +64,7 @@ def _adjust_tts_speed(
80
64
  "segment_tempo_factor": 1.0,
81
65
  "truncated": False,
82
66
  "truncate_reason": "none",
83
- "tts_rate_offset": float(tts_rate_offset or 0.0),
67
+ "tts_rate_offset": float(tts_rate_offset),
84
68
  "audio_duration": current_dur,
85
69
  "placed_audio_duration": current_dur,
86
70
  "global_narration_speed": budget["global_narration_speed"],
@@ -88,13 +72,11 @@ def _adjust_tts_speed(
88
72
  "cumulative_tempo_max": budget["cumulative_tempo_max"],
89
73
  "cumulative_tempo_hard_max": budget["cumulative_tempo_hard_max"],
90
74
  }
91
- if current_dur <= target_duration or current_dur == 0:
75
+ if current_dur <= target_duration:
92
76
  return (str(audio_path), current_dur, meta)
93
77
 
94
78
  ratio = current_dur / target_duration
95
-
96
79
  effective_max = budget["segment_tempo_max"]
97
-
98
80
  if ratio > effective_max:
99
81
  meta.update({
100
82
  "fit_status": "no_safe_fit",
@@ -102,8 +84,6 @@ def _adjust_tts_speed(
102
84
  "truncate_reason": "no_safe_boundary",
103
85
  "placed_audio_duration": 0.0,
104
86
  "needed_tempo_factor": ratio,
105
- "segment_tempo_factor": 1.0,
106
- "tempo_factor": 1.0,
107
87
  })
108
88
  logger(
109
89
  f" TTS 无安全放置: {current_dur:.1f}s 需 x{ratio:.2f},"
@@ -119,34 +99,32 @@ def _adjust_tts_speed(
119
99
  "-filter:a", f"atempo={tempo:.6f}",
120
100
  "-ar", "44100", "-ac", "1", str(adjusted_path)]
121
101
  result = command_runner(cmd)
122
- if result.returncode == 0:
123
- new_dur = duration_probe(adjusted_path)
124
- if new_dur > target_duration + (1.0 / 44100.0):
125
- adjusted_path.unlink(missing_ok=True)
126
- meta.update({
127
- "fit_status": "no_safe_fit",
128
- "blocking": True,
129
- "truncate_reason": "no_safe_boundary",
130
- "placed_audio_duration": 0.0,
131
- "needed_tempo_factor": new_dur / target_duration,
132
- })
133
- logger(
134
- f" TTS 加速后仍超出安全窗口 {new_dur - target_duration:.3f}s;"
135
- "禁止裁尾,交由 Agent 缩短/移动文本"
136
- )
137
- return (str(audio_path), current_dur, meta)
102
+ if result.returncode != 0:
103
+ raise RuntimeError(f"TTS 加速失败 {audio_path}: {result.stderr}")
104
+ new_dur = duration_probe(adjusted_path)
105
+ if new_dur > target_duration + (1.0 / 44100.0):
106
+ adjusted_path.unlink(missing_ok=True)
138
107
  meta.update({
139
- "fit_status": "tempo_adjusted",
140
- "tempo_factor": tempo,
141
- "segment_tempo_factor": tempo,
142
- "placed_audio_duration": new_dur,
143
- "effective_tempo": budget["global_narration_speed"] * budget["tts_rate_factor"] * tempo,
108
+ "fit_status": "no_safe_fit",
109
+ "blocking": True,
110
+ "truncate_reason": "no_safe_boundary",
111
+ "placed_audio_duration": 0.0,
112
+ "needed_tempo_factor": new_dur / target_duration,
144
113
  })
145
- logger(f" TTS 温和加速: {current_dur:.1f}s → {new_dur:.1f}s (x{tempo:.2f})")
146
- return (str(adjusted_path), new_dur, meta)
147
- meta["fit_status"] = "speed_adjust_failed"
148
- meta["truncate_reason"] = "resample_failed"
149
- return (str(audio_path), current_dur, meta)
114
+ logger(
115
+ f" TTS 加速后仍超出安全窗口 {new_dur - target_duration:.3f}s;"
116
+ "禁止裁尾,交由 Agent 缩短/移动文本"
117
+ )
118
+ return (str(audio_path), current_dur, meta)
119
+ meta.update({
120
+ "fit_status": "tempo_adjusted",
121
+ "tempo_factor": tempo,
122
+ "segment_tempo_factor": tempo,
123
+ "placed_audio_duration": new_dur,
124
+ "effective_tempo": budget["global_narration_speed"] * budget["tts_rate_factor"] * tempo,
125
+ })
126
+ logger(f" TTS 温和加速: {current_dur:.1f}s → {new_dur:.1f}s (x{tempo:.2f})")
127
+ return (str(adjusted_path), new_dur, meta)
150
128
 
151
129
 
152
130
  def _edge_quiet_samples(pcm16_mono, sample_count, *, from_start, threshold=260):
@@ -176,6 +154,16 @@ def _speech_safe_fade_lengths(pcm16_mono, sample_count, sample_rate, configured_
176
154
  return min(configured, max(anti_click, leading)), min(configured, max(anti_click, trailing))
177
155
 
178
156
 
157
+ def _unplaced(seg, at, fit_status, reason, *, blocking=False):
158
+ """Record a segment that contributes no audio: a zero-width window at `at` seconds."""
159
+ seg["actual_place_start"] = at
160
+ seg["actual_place_end"] = at
161
+ seg["placed_audio_duration"] = 0.0
162
+ seg["fit_status"] = fit_status
163
+ seg["truncate_reason"] = reason
164
+ seg["blocking"] = blocking
165
+
166
+
179
167
  def _build_timed_narration(
180
168
  tts_segments,
181
169
  output_wav,
@@ -192,88 +180,46 @@ def _build_timed_narration(
192
180
  buffer = bytearray(total_samples * 2)
193
181
  last_written_end = 0 # 追踪已写入位置,防止重叠
194
182
  prev_pause_samples = 0 # 前一段的 pause_after_ms,控制段间间隔
195
- skipped_count = 0 # 因 WAV 缺失/损坏/重采样失败而被跳过的段数
183
+ skipped_count = 0 # 因 WAV 缺失或无安全放置而被跳过的段数
196
184
  placed_count = 0 # 真正写入音频的段数;防止"成功"生成全静音旁白
197
185
  no_safe_fit_count = 0 # 超预算但不能安全截断;交由 QC/manifest 阻断
198
186
  prev_authored_end = None # 上一段作者标注的结束时间,用于判断"段落"边界
199
- run_gap = float(CONFIG.get("narration_run_gap_seconds", 1.6)) # 作者留白 > 此值 = 新段落
200
- tighten = bool(CONFIG.get("narration_tighten", True))
201
- tight_pause_samples = int(max(0.0, float(CONFIG.get("narration_tight_pause_seconds", 0.35))) * sample_rate)
187
+ run_gap = CONFIG["narration_run_gap_seconds"] # 作者留白 > 此值 = 新段落
188
+ tighten = CONFIG["narration_tighten"]
189
+ tight_pause_samples = int(CONFIG["narration_tight_pause_seconds"] * sample_rate)
202
190
  # 漂移上限:收紧时一句最多比作者标注的时间提前 max_pull 秒,避免整段解说被全部压到前面、与画面脱节
203
- max_pull_samples = int(max(0.0, float(CONFIG.get("narration_max_pull_seconds", 2.5))) * sample_rate)
191
+ max_pull_samples = int(CONFIG["narration_max_pull_seconds"] * sample_rate)
192
+ configured_delay = CONFIG["narration_delay_seconds"]
193
+ tail_pad = CONFIG["narration_tail_pad_seconds"]
204
194
 
205
195
  for seg in tts_segments:
206
- seg.setdefault("segment_audio_schema_version", SEGMENT_AUDIO_SCHEMA_VERSION)
207
- seg.setdefault("narration", str(seg.get("narration") or seg.get("spoken_text") or ""))
208
- seg.setdefault("spoken_text", str(seg.get("spoken_text") or seg.get("narration") or ""))
209
- seg.setdefault("truncated", False)
210
- seg.setdefault("truncate_reason", "none")
211
- seg.setdefault("fit_status", "pending_assembly")
212
- seg.setdefault("blocking", False)
213
- seg.setdefault("segment_tempo_factor", 1.0)
214
- seg.setdefault("global_narration_speed", float(CONFIG.get("narration_speed", 1.0) or 1.0))
215
- rate_factor = 1.0 + float(seg.get("tts_rate_offset", 0.0) or 0.0)
216
- seg.setdefault("effective_tempo", float(seg["global_narration_speed"]) * rate_factor * float(seg.get("segment_tempo_factor", 1.0) or 1.0))
217
- seg.setdefault("rms_dbfs_before", None)
218
- seg.setdefault("rms_dbfs_after", None)
219
- seg.setdefault("peak_after", None)
220
196
  wav_path = seg["audio_path"]
221
- seg_pause_ms = seg.get("pause_after_ms", CONFIG.get("breath_ms", 250))
197
+ pause_samples = int(seg.get("pause_after_ms", CONFIG["breath_ms"]) * sample_rate / 1000)
222
198
  # 段落收紧:同一段落内(与上一句作者留白 <= run_gap)把这一句紧贴上一句的实际收尾播放,
223
199
  # 句间间隔固定为 tight_pause,不受 slot 内居中延迟 / TTS 时长波动影响。段落之间(作者特意留
224
200
  # 的大留白,让精彩原声透出)才放回原声。这样句间间隔稳定、不会出现"一句解说一段空白"。
225
- cur_authored_start = float(seg.get("start", 0.0))
201
+ cur_authored_start = float(seg["start"])
226
202
  is_run_start = (placed_count == 0 or prev_authored_end is None
227
203
  or cur_authored_start - prev_authored_end > run_gap)
228
- prev_authored_end = float(seg.get("end", cur_authored_start))
204
+ prev_authored_end = float(seg["end"])
229
205
 
230
206
  if not os.path.exists(wav_path):
231
- seg["actual_place_start"] = seg["start"]
232
- seg["actual_place_end"] = seg["start"]
233
- seg["placed_audio_duration"] = 0.0
234
- seg["fit_status"] = "skipped"
235
- seg["truncate_reason"] = "missing_wav"
236
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
207
+ _unplaced(seg, seg["start"], "skipped", "missing_wav")
208
+ prev_pause_samples = pause_samples
237
209
  skipped_count += 1
238
210
  continue
239
211
 
240
- # WAV 格式验证 + 采样率检查(合并为一次 wave.open)
241
212
  original_wav_path = wav_path
242
- _do_resample = False
243
- try:
244
- with wave.open(wav_path, 'rb') as wf_check:
245
- wf_channels = wf_check.getnchannels()
246
- wf_sampwidth = wf_check.getsampwidth()
247
- if wf_sampwidth != 2 or wf_channels != 1:
248
- logger(f" 跳过非标准 WAV: {wav_path} (channels={wf_channels}, sampwidth={wf_sampwidth}), 需要 mono 16-bit")
249
- seg["actual_place_start"] = seg["start"]
250
- seg["actual_place_end"] = seg["start"]
251
- seg["placed_audio_duration"] = 0.0
252
- seg["fit_status"] = "skipped"
253
- seg["truncate_reason"] = "resample_failed"
254
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
255
- skipped_count += 1
256
- continue
257
- if wf_check.getframerate() != sample_rate:
258
- _do_resample = True
259
- except Exception as e:
260
- logger(f" WAV 读取失败: {wav_path}: {e}")
261
- seg["actual_place_start"] = seg["start"]
262
- seg["actual_place_end"] = seg["start"]
263
- seg["placed_audio_duration"] = 0.0
264
- seg["fit_status"] = "skipped"
265
- seg["truncate_reason"] = "missing_wav"
266
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
267
- skipped_count += 1
268
- continue
213
+ with wave.open(wav_path, "rb") as wf_check:
214
+ needs_resample = (
215
+ wf_check.getnchannels(), wf_check.getsampwidth(), wf_check.getframerate()
216
+ ) != (1, 2, sample_rate)
269
217
 
270
218
  tts_rate_offset = seg.get("tts_rate_offset", 0.0)
271
- tts_dur = seg.get("audio_duration", 0)
219
+ tts_dur = seg["audio_duration"]
272
220
 
273
- configured_delay = max(0.0, float(CONFIG.get("narration_delay_seconds", 0.0) or 0.0))
274
- tail_pad = max(0.0, float(CONFIG.get("narration_tail_pad_seconds", 0.1) or 0.0))
275
221
  slot_duration = max(0.0, float(seg["end"]) - float(seg["start"]))
276
- max_delay = max(0.0, slot_duration - float(tts_dur or 0.0) - tail_pad)
222
+ max_delay = max(0.0, slot_duration - tts_dur - tail_pad)
277
223
  narration_delay = min(configured_delay, max_delay)
278
224
  start_sample = int((seg["start"] + narration_delay) * sample_rate)
279
225
  end_boundary = int(min(seg["end"], video_duration) * sample_rate)
@@ -294,59 +240,43 @@ def _build_timed_narration(
294
240
  available_samples = end_boundary - actual_start
295
241
  available_duration = max(available_samples / sample_rate, 0)
296
242
  if tts_dur > available_duration > 0:
297
- adjusted_result = adjust_speed(
298
- wav_path, available_duration, work_dir, tts_rate_offset)
299
- if len(adjusted_result) == 2:
300
- wav_path, _actual_dur = adjusted_result
301
- budget = narration_tempo_budget(tts_rate_offset)
302
- fit_meta = {
303
- "fit_status": "tempo_adjusted" if wav_path != original_wav_path else "fits",
304
- "segment_tempo_factor": 1.0,
305
- "effective_tempo": budget["global_narration_speed"],
306
- "global_narration_speed": budget["global_narration_speed"],
307
- "truncate_reason": "none",
308
- }
309
- else:
310
- wav_path, _actual_dur, fit_meta = adjusted_result
243
+ wav_path, _actual_dur, fit_meta = adjust_speed(wav_path, available_duration, tts_rate_offset)
311
244
  seg.update({
312
245
  "fit_status": fit_meta["fit_status"],
313
- "segment_tempo_factor": fit_meta.get("segment_tempo_factor", 1.0),
314
- "effective_tempo": fit_meta.get("effective_tempo", seg.get("effective_tempo")),
315
- "global_narration_speed": fit_meta.get("global_narration_speed", seg.get("global_narration_speed")),
316
- "blocking": bool(fit_meta.get("blocking", False)),
246
+ "segment_tempo_factor": fit_meta["segment_tempo_factor"],
247
+ "effective_tempo": fit_meta["effective_tempo"],
248
+ "global_narration_speed": fit_meta["global_narration_speed"],
249
+ "blocking": fit_meta["blocking"],
317
250
  })
318
251
  if fit_meta["fit_status"] == "no_safe_fit":
319
- seg["actual_place_start"] = actual_start / sample_rate
320
- seg["actual_place_end"] = actual_start / sample_rate
321
- seg["placed_audio_duration"] = 0.0
322
- seg["truncate_reason"] = fit_meta["truncate_reason"]
323
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
252
+ _unplaced(seg, actual_start / sample_rate, "no_safe_fit",
253
+ fit_meta["truncate_reason"], blocking=True)
254
+ prev_pause_samples = pause_samples
324
255
  skipped_count += 1
325
256
  no_safe_fit_count += 1
326
257
  continue
327
258
  else:
328
259
  budget = narration_tempo_budget(tts_rate_offset)
329
- seg["fit_status"] = "fits"
330
- seg["segment_tempo_factor"] = 1.0
331
- seg["global_narration_speed"] = budget["global_narration_speed"]
332
- seg["effective_tempo"] = budget["global_narration_speed"] * budget["tts_rate_factor"]
260
+ seg.update({
261
+ "fit_status": "fits",
262
+ "segment_tempo_factor": 1.0,
263
+ "global_narration_speed": budget["global_narration_speed"],
264
+ "effective_tempo": budget["global_narration_speed"] * budget["tts_rate_factor"],
265
+ "blocking": False,
266
+ })
333
267
 
334
268
  # _adjust_tts_speed 输出固定 44100Hz mono 16bit,若文件被替换则无需 resample
335
269
  if wav_path != original_wav_path:
336
- _do_resample = False
337
- if _do_resample:
338
- tmp_path = str(Path(work_dir) / f"_rs_{seg.get('index', 0)}.wav")
270
+ needs_resample = False
271
+ if needs_resample:
272
+ tmp_path = str(Path(work_dir) / f"_rs_{seg['index']}.wav")
339
273
  rs_result = command_runner(["ffmpeg", "-y", "-i", wav_path,
340
274
  "-ar", str(sample_rate), "-ac", "1",
341
275
  "-acodec", "pcm_s16le", tmp_path])
342
- if rs_result.returncode != 0 or not os.path.exists(tmp_path):
343
- logger(f" 重采样失败,跳过本段: {wav_path}: {rs_result.stderr}")
344
- seg["actual_place_start"] = seg["start"]
345
- seg["actual_place_end"] = seg["start"]
346
- seg["placed_audio_duration"] = 0.0
347
- seg["fit_status"] = "skipped"
348
- seg["truncate_reason"] = "resample_failed"
349
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
276
+ if rs_result.returncode != 0:
277
+ logger(f" 跳过: 重采样失败 {wav_path}: {rs_result.stderr}")
278
+ _unplaced(seg, seg["start"], "skipped", "resample_failed")
279
+ prev_pause_samples = pause_samples
350
280
  skipped_count += 1
351
281
  continue
352
282
  wav_path = tmp_path
@@ -361,13 +291,8 @@ def _build_timed_narration(
361
291
 
362
292
  if write_samples <= 0 or available <= 0:
363
293
  logger(f" 跳过: {seg['start']:.1f}s-{seg['end']:.1f}s (无空间)")
364
- seg["actual_place_start"] = seg["start"]
365
- seg["actual_place_end"] = seg["start"]
366
- seg["placed_audio_duration"] = 0.0
367
- seg["fit_status"] = "no_safe_fit"
368
- seg["blocking"] = True
369
- seg["truncate_reason"] = "no_room"
370
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
294
+ _unplaced(seg, seg["start"], "no_safe_fit", "no_room", blocking=True)
295
+ prev_pause_samples = pause_samples
371
296
  no_safe_fit_count += 1
372
297
  continue
373
298
 
@@ -376,14 +301,9 @@ def _build_timed_narration(
376
301
  # consonant/vowel release. _adjust_tts_speed must produce a complete file
377
302
  # that fits; otherwise block and ask the Agent to shorten/move the block.
378
303
  over = (audio_samples - available) / sample_rate
379
- logger(f" TTS 无安全放置: 段 {seg.get('index', '?')} 超出可用窗口 {over:.3f}s;禁止裁尾,交由 QC 阻断")
380
- seg["actual_place_start"] = actual_start / sample_rate
381
- seg["actual_place_end"] = actual_start / sample_rate
382
- seg["placed_audio_duration"] = 0.0
383
- seg["fit_status"] = "no_safe_fit"
384
- seg["blocking"] = True
385
- seg["truncate_reason"] = "no_safe_boundary"
386
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
304
+ logger(f" TTS 无安全放置: 段 {seg['index']} 超出可用窗口 {over:.3f}s;禁止裁尾,交由 QC 阻断")
305
+ _unplaced(seg, actual_start / sample_rate, "no_safe_fit", "no_safe_boundary", blocking=True)
306
+ prev_pause_samples = pause_samples
387
307
  skipped_count += 1
388
308
  no_safe_fit_count += 1
389
309
  continue
@@ -394,33 +314,23 @@ def _build_timed_narration(
394
314
  if last_written_end >= actual_start + write_samples:
395
315
  logger(f" 跳过重叠段: {actual_start/sample_rate:.1f}s "
396
316
  f"(与前段重叠 {overlap_ms:.0f}ms)")
397
- seg["actual_place_start"] = seg["start"]
398
- seg["actual_place_end"] = seg["start"]
399
- seg["placed_audio_duration"] = 0.0
400
- seg["fit_status"] = "no_safe_fit"
401
- seg["blocking"] = True
402
- seg["truncate_reason"] = "no_room"
403
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
317
+ _unplaced(seg, seg["start"], "no_safe_fit", "no_room", blocking=True)
318
+ prev_pause_samples = pause_samples
404
319
  no_safe_fit_count += 1
405
320
  continue
406
321
  actual_start = last_written_end
407
322
  available = end_boundary - actual_start
408
323
  if write_samples > available:
409
324
  logger(f" 重叠 {overlap_ms:.0f}ms 后无安全完整窗口,跳过")
410
- seg["actual_place_start"] = actual_start / sample_rate
411
- seg["actual_place_end"] = actual_start / sample_rate
412
- seg["placed_audio_duration"] = 0.0
413
- seg["fit_status"] = "no_safe_fit"
414
- seg["blocking"] = True
415
- seg["truncate_reason"] = "no_safe_boundary"
416
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
325
+ _unplaced(seg, actual_start / sample_rate, "no_safe_fit", "no_safe_boundary", blocking=True)
326
+ prev_pause_samples = pause_samples
417
327
  skipped_count += 1
418
328
  no_safe_fit_count += 1
419
329
  continue
420
330
 
421
331
  # fade-in / fade-out(在 overlap 裁剪之后应用,确保正确的音频包络)
422
332
  fade_in_len, fade_out_len = _speech_safe_fade_lengths(
423
- wf_data, write_samples, sample_rate, CONFIG.get("fade_ms", 120)
333
+ wf_data, write_samples, sample_rate, CONFIG["fade_ms"]
424
334
  )
425
335
  for i in range(fade_in_len):
426
336
  gain = i / fade_in_len
@@ -440,7 +350,7 @@ def _build_timed_narration(
440
350
  # Persist the exact complete per-beat PCM used by the canonical mix. Editable
441
351
  # exports must reference this file, not the longer pre-fit TTS input; otherwise
442
352
  # their timeline_end silently chops the final word even when ffmpeg is correct.
443
- placed_path = Path(work_dir) / f"_placed_{int(seg.get('index', placed_count)):04d}.wav"
353
+ placed_path = Path(work_dir) / f"_placed_{seg['index']:04d}.wav"
444
354
  with wave.open(str(placed_path), "wb") as placed_wav:
445
355
  placed_wav.setnchannels(1)
446
356
  placed_wav.setsampwidth(2)
@@ -452,12 +362,8 @@ def _build_timed_narration(
452
362
  seg["actual_place_start"] = actual_start / sample_rate
453
363
  seg["actual_place_end"] = (actual_start + write_samples) / sample_rate
454
364
  seg["placed_audio_duration"] = write_samples / sample_rate
455
- if seg.get("fit_status") == "pending_assembly":
456
- seg["fit_status"] = "fits"
457
- if seg.get("truncate_reason") in (None, ""):
458
- seg["truncate_reason"] = "none"
459
365
  last_written_end = actual_start + write_samples
460
- prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
366
+ prev_pause_samples = pause_samples
461
367
  placed_count += 1
462
368
 
463
369
  with wave.open(str(output_wav), "wb") as wf:
@@ -470,7 +376,7 @@ def _build_timed_narration(
470
376
  output_wav.unlink(missing_ok=True)
471
377
  raise RuntimeError(
472
378
  f"全部 {len(tts_segments)} 段解说均被跳过或未能写入"
473
- f"(WAV 缺失/损坏/重采样失败/无可用时间;跳过 {skipped_count} 段),"
379
+ f"(WAV 缺失或无可用时间;跳过 {skipped_count} 段),"
474
380
  "已中止以避免生成无解说视频"
475
381
  )
476
382
 
@@ -1,23 +1,16 @@
1
1
  """Local ffmpeg capability preflight for subtitle burn-in."""
2
2
 
3
+ import shutil
4
+ import subprocess
5
+
3
6
  from lib import CONFIG
4
7
 
5
8
  def _ffmpeg_filters():
6
- """Return ffmpeg's compiled-in filter names.
7
-
8
- This skill keeps a local capability probe so it remains self-contained; keep the
9
- parser behavior aligned through tests.
10
- """
11
- import shutil
12
- import subprocess
13
- ffmpeg = shutil.which("ffmpeg")
14
- if not ffmpeg:
15
- return set()
16
- try:
17
- result = subprocess.run(["ffmpeg", "-hide_banner", "-filters"],
18
- text=True, capture_output=True, timeout=20)
19
- except (OSError, subprocess.SubprocessError):
9
+ """Return ffmpeg's compiled-in filter names."""
10
+ if shutil.which("ffmpeg") is None:
20
11
  return set()
12
+ result = subprocess.run(["ffmpeg", "-hide_banner", "-filters"],
13
+ text=True, capture_output=True, timeout=20)
21
14
  if result.returncode != 0:
22
15
  return set()
23
16
  filters = set()
@@ -30,13 +23,9 @@ def _ffmpeg_filters():
30
23
 
31
24
  def _preflight_burn_subtitles():
32
25
  """Fail before the (re-encoding) render when burn-in is on but ffmpeg lacks the libass
33
- `subtitles` filter. _subtitle_burn_filter burns even the .ass through `subtitles=`, so
34
- that is the required capability. Defense-in-depth: the orchestrator (recap.py) preflights
35
- this earlier, but assemble.py can be run standalone. Only fires when ffmpeg EXISTS but
36
- can't burn — an absent ffmpeg fails the render regardless and would also break the mocked,
37
- ffmpeg-less test environment."""
38
- import shutil
39
- if not CONFIG.get("burn_subtitles", False):
26
+ `subtitles` filter. Only fires when ffmpeg EXISTS but can't burn — an absent ffmpeg fails
27
+ the render regardless."""
28
+ if not CONFIG["burn_subtitles"]:
40
29
  return
41
30
  if shutil.which("ffmpeg") is None:
42
31
  return