@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
@@ -10,7 +10,6 @@ from assemble_constants import (
10
10
  VISUAL_OVERLAYS,
11
11
  VISUAL_QC,
12
12
  _SUPPORTED_VISUAL_OVERLAY_TYPES,
13
- _VISUAL_DELIVERY_FORBIDDEN_KEYS,
14
13
  )
15
14
  from audio_automation import coalesce_duck_windows
16
15
  from audio_mix import _seg_place_window
@@ -21,17 +20,24 @@ from source_subtitles import (
21
20
  _source_subtitle_mask_policy,
22
21
  )
23
22
  from subtitle_core import (
23
+ _measured_subtitle_band,
24
24
  _measured_subtitle_safe_area,
25
25
  _normalize_subtitle_text,
26
26
  _style_for_measured_subtitle_band,
27
27
  _subtitle_style_config,
28
- _validate_measured_subtitle_coordinate_domain,
29
28
  )
30
29
 
30
+ _OVERFLOW_KINDS = {
31
+ "max_lines_exceeded": "line_count",
32
+ "safe_width_exceeded": "line_width",
33
+ "safe_height_exceeded": "safe_area",
34
+ }
35
+
36
+
31
37
  def _visual_text_units(text):
32
38
  """Approximate visual text width in em units for deterministic geometry QC."""
33
39
  units = 0.0
34
- for ch in str(text or ""):
40
+ for ch in text:
35
41
  if ch.isspace():
36
42
  units += 0.35
37
43
  elif ord(ch) < 128:
@@ -41,51 +47,37 @@ def _visual_text_units(text):
41
47
  return units
42
48
 
43
49
 
44
- def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
50
+ def _subtitle_layout_qc(entries, style, safe_area=None):
45
51
  """Machine-check subtitle safe-area/multiline/overflow facts for visual_qc.json."""
46
- canvas = canvas or {}
47
- style = style or _subtitle_style_config(canvas)
48
- play_x = int(style.get("play_res_x") or canvas.get("width") or 1280)
49
- play_y = int(style.get("play_res_y") or canvas.get("height") or 720)
50
- margin_l = int(style.get("margin_l") or 0)
51
- margin_r = int(style.get("margin_r") or 0)
52
- margin_v = int(style.get("margin_v") or 0)
53
- font_size = float(style.get("font_size") or 1)
54
- max_lines = int(CONFIG.get("subtitle_max_lines", 2) or 2)
55
- usable_w = max(1.0, play_x - margin_l - margin_r)
56
- if safe_area:
52
+ play_x = int(style["play_res_x"])
53
+ play_y = int(style["play_res_y"])
54
+ margin_l = int(style["margin_l"])
55
+ margin_r = int(style["margin_r"])
56
+ margin_v = int(style["margin_v"])
57
+ font_size = float(style["font_size"])
58
+ max_lines = CONFIG["subtitle_max_lines"]
59
+ if safe_area is None:
57
60
  safe_area = {
58
- "x": int(safe_area.get("x", margin_l) or 0),
59
- "y": int(safe_area.get("y", margin_v) or 0),
60
- "width": int(safe_area.get("width", safe_area.get("w", play_x - margin_l - margin_r)) or 1),
61
- "height": int(safe_area.get("height", safe_area.get("h", play_y - 2 * margin_v)) or 1),
61
+ "x": margin_l,
62
+ "y": margin_v,
63
+ "width": max(1, play_x - margin_l - margin_r),
64
+ "height": max(1, play_y - 2 * margin_v),
62
65
  "bottom_margin": margin_v,
63
66
  }
64
- usable_w = max(1.0, float(safe_area["width"]))
65
- else:
66
- safe_area = {
67
- "x": margin_l,
68
- "y": margin_v,
69
- "width": max(1, play_x - margin_l - margin_r),
70
- "height": max(1, play_y - 2 * margin_v),
71
- "bottom_margin": margin_v,
72
- }
67
+ usable_w = float(safe_area["width"])
73
68
  line_h = font_size * 1.25
74
69
  overflow_entries = []
75
70
  violations = []
76
71
  multi_line_entries = []
77
72
  max_observed_lines = 0
78
73
  entry_facts = []
79
- for i, entry in enumerate(entries or []):
80
- raw_text = _normalize_subtitle_text(entry.get("text", ""))
81
- lines = [ln for ln in re.split(r"(?:\\N|\n)+", raw_text) if ln != ""]
82
- if not lines:
83
- lines = [""]
74
+ for i, entry in enumerate(entries):
75
+ raw_text = _normalize_subtitle_text(entry["text"])
76
+ lines = [ln for ln in re.split(r"(?:\\N|\n)+", raw_text) if ln != ""] or [""]
84
77
  line_count = len(lines)
85
78
  max_observed_lines = max(max_observed_lines, line_count)
86
- widths = [_visual_text_units(line) * font_size for line in lines]
87
- max_w = max(widths or [0.0])
88
- band_h = line_count * line_h + float(style.get("outline", 0)) * 2 + float(style.get("shadow", 0))
79
+ max_w = max(_visual_text_units(line) * font_size for line in lines)
80
+ band_h = line_count * line_h + float(style["outline"]) * 2 + float(style["shadow"])
89
81
  overflow_reasons = []
90
82
  if line_count > max_lines:
91
83
  overflow_reasons.append("max_lines_exceeded")
@@ -95,8 +87,8 @@ def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
95
87
  overflow_reasons.append("safe_height_exceeded")
96
88
  fact = {
97
89
  "index": i,
98
- "start": round(float(entry.get("start", 0.0) or 0.0), 3),
99
- "end": round(float(entry.get("end", 0.0) or 0.0), 3),
90
+ "start": round(float(entry["start"]), 3),
91
+ "end": round(float(entry["end"]), 3),
100
92
  "line_count": line_count,
101
93
  "max_line_width": round(max_w, 2),
102
94
  "safe_width": round(usable_w, 2),
@@ -109,23 +101,20 @@ def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
109
101
  multi_line_entries.append(i)
110
102
  if overflow_reasons:
111
103
  overflow_entries.append(fact)
112
- for reason in overflow_reasons:
113
- kind = {
114
- "max_lines_exceeded": "line_count",
115
- "safe_width_exceeded": "line_width",
116
- "safe_height_exceeded": "safe_area",
117
- }.get(reason, "safe_area")
118
- violations.append({"index": i, "kind": kind, "reason": reason})
104
+ violations.extend(
105
+ {"index": i, "kind": _OVERFLOW_KINDS[reason], "reason": reason}
106
+ for reason in overflow_reasons
107
+ )
119
108
  return {
120
- "enabled": bool(CONFIG.get("burn_subtitles", False)),
121
- "renderer": "ass" if CONFIG.get("burn_subtitles", False) else "sidecar_srt",
109
+ "enabled": CONFIG["burn_subtitles"],
110
+ "renderer": "ass" if CONFIG["burn_subtitles"] else "sidecar_srt",
122
111
  "style": {
123
112
  "font_size": int(font_size),
124
- "max_chars": int(style.get("max_chars") or 0),
113
+ "max_chars": int(style["max_chars"]),
125
114
  "max_lines": max_lines,
126
115
  "play_res_x": play_x,
127
116
  "play_res_y": play_y,
128
- "alignment": int(style.get("alignment") or 0),
117
+ "alignment": int(style["alignment"]),
129
118
  "margin_l": margin_l,
130
119
  "margin_r": margin_r,
131
120
  "margin_v": margin_v,
@@ -143,45 +132,35 @@ def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
143
132
  }
144
133
 
145
134
 
146
- def _load_visual_overlays(work_dir, *, with_source=False):
135
+ def _load_visual_overlays(work_dir):
136
+ """Return (overlays, source) from the canonical visual_overlays.json handoff."""
147
137
  path = Path(work_dir) / VISUAL_OVERLAYS
148
138
  if not path.exists():
149
- result = ([], {"present": False, "path": str(path), "fingerprint": None})
150
- return result if with_source else result[0]
139
+ return [], {"present": False, "path": str(path), "fingerprint": None}
151
140
  try:
152
141
  data = json.loads(path.read_text(encoding="utf-8"))
153
- except (ValueError, OSError) as exc:
154
- result = ([], {
155
- "present": True,
156
- "path": str(path),
157
- "fingerprint": _artifact_fingerprint(path),
158
- "load_error": "invalid_json",
159
- "load_error_detail": str(exc),
160
- })
161
- return result if with_source else result[0]
142
+ except json.JSONDecodeError as exc:
143
+ raise ValueError(f"{path}: JSON 无效") from exc
144
+ if (
145
+ not isinstance(data, dict)
146
+ or type(data.get("schema_version")) is not int
147
+ or data["schema_version"] != 1
148
+ or not isinstance(data.get("overlays"), list)
149
+ or not all(isinstance(item, dict) for item in data["overlays"])
150
+ ):
151
+ raise ValueError(f"{path}: visual_overlays.json schema 无效")
162
152
  source = {
163
153
  "present": True,
164
154
  "path": str(path),
165
155
  "fingerprint": _artifact_fingerprint(path),
166
- "schema_version": data.get("schema_version") if isinstance(data, dict) else None,
156
+ "schema_version": 1,
167
157
  }
168
- schema_version = data.get("schema_version") if isinstance(data, dict) else None
169
- valid_schema_version = (
170
- isinstance(schema_version, int)
171
- and not isinstance(schema_version, bool)
172
- and schema_version == 1
173
- )
174
- if isinstance(data, dict) and valid_schema_version and isinstance(data.get("overlays"), list):
175
- overlays = data["overlays"]
176
- else:
177
- overlays = []
178
- source["load_error"] = "invalid_schema"
179
- return (overlays, source) if with_source else overlays
158
+ return data["overlays"], source
180
159
 
181
160
 
182
161
  def _escape_drawtext_text(text):
183
162
  return (
184
- str(text or "")
163
+ text
185
164
  .replace("\\", "\\\\")
186
165
  .replace(":", "\\:")
187
166
  .replace("'", "\\'")
@@ -191,44 +170,30 @@ def _escape_drawtext_text(text):
191
170
 
192
171
 
193
172
  def _overlay_time_window(overlay, video_duration):
194
- try:
195
- start = float(overlay.get("start", 0.0) or 0.0)
196
- except (TypeError, ValueError):
197
- start = 0.0
198
- try:
199
- end = float(overlay.get("end", video_duration) or video_duration)
200
- except (TypeError, ValueError):
201
- end = float(video_duration or 0.0)
202
- end = max(start, end)
203
- return start, end
173
+ start = float(overlay.get("start", 0.0))
174
+ end = float(overlay.get("end", video_duration))
175
+ return start, max(start, end)
204
176
 
205
177
 
206
178
  def _overlay_bbox(overlay, canvas, *, default_y):
207
- width = int((canvas or {}).get("width") or 1280)
208
- height = int((canvas or {}).get("height") or 720)
209
- text = str(overlay.get("text") or "")
210
- font_size = int(overlay.get("font_size") or max(18, round(height * 0.045)))
179
+ width = canvas["width"]
180
+ height = canvas["height"]
181
+ text = overlay["text"]
182
+ font_size = int(overlay.get("font_size", max(18, round(height * 0.045))))
211
183
  lines = [ln for ln in text.splitlines() if ln.strip()] or [text]
212
- max_w = max((_visual_text_units(ln) * font_size for ln in lines), default=0.0)
184
+ max_w = max(_visual_text_units(ln) * font_size for ln in lines)
213
185
  text_h = len(lines) * font_size * 1.25
214
- if overlay.get("type") == "top_title":
186
+ if overlay["type"] == "top_title":
215
187
  x = max(0.0, (width - max_w) / 2)
216
- y = float(overlay.get("y", default_y) or default_y)
188
+ y = float(overlay.get("y", default_y))
217
189
  else:
218
- raw_x = overlay.get("x", 0.08)
219
- raw_y = overlay.get("y", 0.25)
220
- try:
221
- x = float(raw_x)
222
- if 0.0 <= x <= 1.0:
223
- x *= width
224
- except (TypeError, ValueError):
225
- x = width * 0.08
226
- try:
227
- y = float(raw_y)
228
- if 0.0 <= y <= 1.0:
229
- y *= height
230
- except (TypeError, ValueError):
231
- y = height * 0.25
190
+ # Fractions of the canvas in [0, 1] are normalized coordinates; larger values are pixels.
191
+ x = float(overlay.get("x", 0.08))
192
+ y = float(overlay.get("y", 0.25))
193
+ if 0.0 <= x <= 1.0:
194
+ x *= width
195
+ if 0.0 <= y <= 1.0:
196
+ y *= height
232
197
  return {
233
198
  "x": round(x, 2),
234
199
  "y": round(y, 2),
@@ -246,19 +211,15 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
246
211
  Only two semantic renderers are supported: top_title and inline_label_or_callout.
247
212
  Unsupported types are QC-blocking and deliberately do not silently render.
248
213
  """
249
- overlays, source = _load_visual_overlays(work_dir, with_source=True)
250
- height = int((canvas or {}).get("height") or 720)
251
- default_top_y = max(24, round(height * 0.05))
214
+ overlays, source = _load_visual_overlays(work_dir)
215
+ default_top_y = max(24, round(canvas["height"] * 0.05))
252
216
  filters = []
253
217
  facts = []
254
218
  unsupported = []
255
219
  overflow = []
256
220
  for idx, overlay in enumerate(overlays):
257
- if not isinstance(overlay, dict):
258
- unsupported.append({"index": idx, "type": None, "reason": "overlay_not_object"})
259
- continue
260
- typ = str(overlay.get("type") or "").strip()
261
- text = str(overlay.get("text") or "").strip()
221
+ typ = overlay.get("type")
222
+ text = overlay.get("text", "").strip()
262
223
  if typ not in _SUPPORTED_VISUAL_OVERLAY_TYPES:
263
224
  unsupported.append({"index": idx, "type": typ, "reason": "unsupported_overlay_type"})
264
225
  continue
@@ -297,9 +258,8 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
297
258
  })
298
259
  qc = {
299
260
  "source": source,
300
- "load_error": source.get("load_error"),
301
261
  "supported_types": sorted(_SUPPORTED_VISUAL_OVERLAY_TYPES),
302
- "present": bool(source.get("present")),
262
+ "present": source["present"],
303
263
  "count": len(overlays),
304
264
  "rendered": len(facts),
305
265
  "facts": facts,
@@ -309,49 +269,33 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
309
269
  return filters, qc
310
270
 
311
271
 
312
- def _visual_qc_has_forbidden_delivery_facts(value):
313
- if isinstance(value, dict):
314
- for key, child in value.items():
315
- if key in _VISUAL_DELIVERY_FORBIDDEN_KEYS:
316
- return True
317
- if _visual_qc_has_forbidden_delivery_facts(child):
318
- return True
319
- elif isinstance(value, list):
320
- return any(_visual_qc_has_forbidden_delivery_facts(item) for item in value)
321
- return False
322
-
323
-
324
272
  def _build_visual_qc(tts_segments, work_dir, video_duration, canvas, *, overlay_qc=None, mask_filter=None):
325
273
  entries = _combined_subtitle_entries(tts_segments, work_dir, video_duration)
326
274
  style = _style_for_measured_subtitle_band(_subtitle_style_config(canvas), canvas)
327
275
  subtitle_layout = _subtitle_layout_qc(
328
- entries, canvas, style, safe_area=_measured_subtitle_safe_area(style, canvas)
276
+ entries, style, safe_area=_measured_subtitle_safe_area(style, canvas)
329
277
  )
330
278
  mask = _source_subtitle_mask_policy(work_dir)
331
- ratio = None
332
- if mask.get("active"):
333
- ratio = max(0.0, min(0.5, float(CONFIG.get("source_subtitle_mask_ratio", 0.14) or 0.0)))
334
279
  mask.update({
335
- "ratio": ratio,
280
+ "ratio": min(0.5, CONFIG["source_subtitle_mask_ratio"]) if mask["active"] else None,
336
281
  "filter": "drawbox" if mask_filter else None,
337
- "opacity": float(CONFIG.get("subtitle_mask_opacity", 0.6)),
338
- "timing": str(CONFIG.get("source_subtitle_mask_timing", "narration")),
339
- "subtitle_y_top": int(CONFIG.get("subtitle_y_top", -1)),
340
- "subtitle_y_bot": int(CONFIG.get("subtitle_y_bot", -1)),
282
+ "opacity": CONFIG["subtitle_mask_opacity"],
283
+ "timing": CONFIG["source_subtitle_mask_timing"],
284
+ "subtitle_y_top": CONFIG["subtitle_y_top"],
285
+ "subtitle_y_bot": CONFIG["subtitle_y_bot"],
341
286
  })
342
- overlay_qc = overlay_qc or _visual_overlay_filters(work_dir, canvas, video_duration)[1]
287
+ if overlay_qc is None:
288
+ overlay_qc = _visual_overlay_filters(work_dir, canvas, video_duration)[1]
343
289
  blocking_codes = []
344
- if mask.get("blocking"):
290
+ if mask["blocking"]:
345
291
  blocking_codes.append("mask_policy_not_explicit")
346
- if subtitle_layout.get("overflow"):
292
+ if subtitle_layout["overflow"]:
347
293
  blocking_codes.append("subtitle_overflow")
348
- if overlay_qc.get("load_error"):
349
- blocking_codes.append("invalid_visual_overlays_json")
350
- if overlay_qc.get("unsupported"):
294
+ if overlay_qc["unsupported"]:
351
295
  blocking_codes.append("unsupported_visual_overlay")
352
- if overlay_qc.get("overflow"):
296
+ if overlay_qc["overflow"]:
353
297
  blocking_codes.append("visual_overlay_overflow")
354
- qc = {
298
+ return {
355
299
  "schema_version": 1,
356
300
  "artifact": VISUAL_QC,
357
301
  "verdict": "FAIL" if blocking_codes else "PASS",
@@ -359,36 +303,31 @@ def _build_visual_qc(tts_segments, work_dir, video_duration, canvas, *, overlay_
359
303
  "blocking_codes": blocking_codes,
360
304
  "geometry": {
361
305
  "canvas": {
362
- "width": int(canvas.get("width", 1280)),
363
- "height": int(canvas.get("height", 720)),
364
- "fps": float(canvas.get("fps", 30.0)),
306
+ "width": canvas["width"],
307
+ "height": canvas["height"],
308
+ "fps": canvas["fps"],
365
309
  },
366
310
  "storage": {
367
- "width": int(canvas.get("storage_width", canvas.get("width", 1280))),
368
- "height": int(canvas.get("storage_height", canvas.get("height", 720))),
311
+ "width": canvas["storage_width"],
312
+ "height": canvas["storage_height"],
369
313
  },
370
- "rotation": int(canvas.get("rotation", 0) or 0),
371
- "sample_aspect_ratio": canvas.get("sample_aspect_ratio", "1:1"),
372
- "display_aspect_ratio": canvas.get("display_aspect_ratio"),
314
+ "rotation": canvas["rotation"],
315
+ "sample_aspect_ratio": canvas["sample_aspect_ratio"],
316
+ "display_aspect_ratio": canvas["display_aspect_ratio"],
373
317
  },
374
318
  "subtitles": subtitle_layout,
375
319
  "mask": mask,
376
320
  "overlays": overlay_qc,
377
321
  "summary": {
378
- "subtitle_entries": subtitle_layout.get("entries", 0),
379
- "subtitle_overflow": bool(subtitle_layout.get("overflow")),
380
- "subtitle_multi_line": bool(subtitle_layout.get("multi_line")),
381
- "mask_policy": mask.get("policy"),
382
- "mask_active": bool(mask.get("active")),
383
- "overlay_rendered": int(overlay_qc.get("rendered", 0) or 0),
384
- "overlay_unsupported": len(overlay_qc.get("unsupported") or []),
322
+ "subtitle_entries": subtitle_layout["entries"],
323
+ "subtitle_overflow": subtitle_layout["overflow"],
324
+ "subtitle_multi_line": subtitle_layout["multi_line"],
325
+ "mask_policy": mask["policy"],
326
+ "mask_active": mask["active"],
327
+ "overlay_rendered": overlay_qc["rendered"],
328
+ "overlay_unsupported": len(overlay_qc["unsupported"]),
385
329
  },
386
330
  }
387
- if _visual_qc_has_forbidden_delivery_facts(qc):
388
- qc["verdict"] = "FAIL"
389
- qc["blocking"] = True
390
- qc["blocking_codes"].append("visual_qc_contains_delivery_fact")
391
- return qc
392
331
 
393
332
 
394
333
  def _write_visual_qc(work_dir, qc):
@@ -427,9 +366,7 @@ def _output_downscale_filter(max_h):
427
366
  return f"scale=-2:'2*trunc(min(ih,{max_h})/2)':flags=lanczos"
428
367
 
429
368
 
430
- def _source_subtitle_mask_filter(
431
- canvas=None, work_dir=None, tts_segments=None, video_duration=None
432
- ):
369
+ def _source_subtitle_mask_filter(canvas, work_dir, tts_segments, video_duration):
433
370
  """Return source-subtitle drawbox filters, optionally scoped to narration windows.
434
371
 
435
372
  Many source videos (e.g. 庆余年) ship hardcoded subtitles; without this the recap
@@ -438,71 +375,56 @@ def _source_subtitle_mask_filter(
438
375
  remain configurable.
439
376
  """
440
377
  policy = _source_subtitle_mask_policy(work_dir)
441
- if not policy.get("active"):
378
+ if not policy["active"]:
442
379
  return None
443
- opacity = max(0.0, min(1.0, float(CONFIG.get("subtitle_mask_opacity", 0.6))))
444
-
445
- _validate_measured_subtitle_coordinate_domain(canvas)
380
+ opacity = CONFIG["subtitle_mask_opacity"]
381
+ timing = CONFIG["source_subtitle_mask_timing"]
382
+ if timing not in {"all", "narration"}:
383
+ raise ValueError(f"SOURCE_SUBTITLE_MASK_TIMING 必须是 all 或 narration,当前为 {timing!r}")
446
384
 
447
- canvas_h = int((canvas or {}).get("height", 0) or 0)
448
- y_top = int(CONFIG.get("subtitle_y_top", -1))
449
- y_bot = int(CONFIG.get("subtitle_y_bot", -1))
450
- custom_band = canvas_h > 0 and 0 <= y_top < y_bot <= canvas_h
451
- if custom_band:
452
- padding = int(CONFIG.get("subtitle_mask_padding", 4) or 0)
385
+ band = _measured_subtitle_band(canvas)
386
+ if band is not None:
387
+ y_top, y_bot = band
388
+ padding = CONFIG["subtitle_mask_padding"]
453
389
  mask_top = max(0, y_top - padding)
454
- mask_bot = min(canvas_h, y_bot + padding)
390
+ mask_bot = min(canvas["height"], y_bot + padding)
455
391
  geometry = f"x=0:y={mask_top}:w=iw:h={mask_bot - mask_top}"
456
392
  else:
457
- ratio = max(0.0, min(0.5, float(CONFIG.get("source_subtitle_mask_ratio", 0.14) or 0.0)))
458
393
  # Our subtitle cues are one line. Keep the mask large enough for that line and its
459
394
  # margin, but never regress to the old two-line bar that hid ~23% of the image.
460
395
  style = _subtitle_style_config(canvas)
461
- play_res_y = max(1.0, float(style["play_res_y"]))
396
+ play_res_y = float(style["play_res_y"])
462
397
  line_h = float(style["font_size"]) * 1.25
463
398
  pad = 10.0 * play_res_y / SUBTITLE_STYLE_REF_H
464
399
  sub_band = (float(style["margin_v"]) + line_h + pad) / play_res_y
465
- ratio = min(0.5, max(ratio, sub_band))
466
- if ratio <= 0:
467
- return None
400
+ ratio = min(0.5, max(CONFIG["source_subtitle_mask_ratio"], sub_band))
468
401
  geometry = f"x=0:y=ih-ih*{ratio:.3f}:w=iw:h=ih*{ratio:.3f}"
469
402
 
470
403
  base = f"drawbox={geometry}:color=black@{opacity:.2f}:t=fill"
471
- timing = str(CONFIG.get("source_subtitle_mask_timing", "narration") or "narration").lower()
472
- if timing not in {"all", "narration"}:
473
- timing = "narration"
474
404
  filters = []
475
- if timing == "all" and opacity > 0:
476
- filters.append(base)
477
- elif timing == "narration" and opacity > 0:
478
- windows = []
479
- for seg in tts_segments or []:
480
- if not isinstance(seg, dict):
481
- continue
482
- start, end = _seg_place_window(seg)
483
- try:
484
- start, end = float(start), float(end)
485
- except (TypeError, ValueError):
486
- continue
487
- if end <= start:
488
- continue
489
- windows.append((start, end, 0.0))
490
- # Avoid overlapping drawboxes: stacking two 60%-black masks would darken the overlap
491
- # to 84%. Coalescing also keeps long filter chains smaller.
492
- filters.extend(
493
- f"{base}:enable='between(t,{start:.3f},{end:.3f})'"
494
- for start, end, _ in coalesce_duck_windows(windows, bridge=0.001)
495
- )
405
+ if opacity > 0:
406
+ if timing == "all":
407
+ filters.append(base)
408
+ else:
409
+ windows = [
410
+ (start, end, 0.0)
411
+ for start, end in map(_seg_place_window, tts_segments)
412
+ if end > start
413
+ ]
414
+ # Avoid overlapping drawboxes: stacking two 60%-black masks would darken the
415
+ # overlap to 84%. Coalescing also keeps long filter chains smaller.
416
+ filters.extend(
417
+ f"{base}:enable='between(t,{start:.3f},{end:.3f})'"
418
+ for start, end, _ in coalesce_duck_windows(windows, bridge=0.001)
419
+ )
496
420
 
497
421
  # A translucent mask deliberately leaves the source glyphs visible. Whenever we burn a
498
422
  # replacement original-dialogue subtitle into a gap, cover that exact window opaquely first;
499
423
  # otherwise the source hard-sub and replacement text are stacked on top of each other.
500
- if video_duration is not None and not (timing == "all" and opacity >= 1.0 - 1e-9):
501
- replacement_entries = _original_gap_subtitle_entries(
502
- tts_segments or [], work_dir, video_duration
503
- )
424
+ if not (timing == "all" and opacity >= 1.0 - 1e-9):
504
425
  replacement_windows = [
505
- (entry["start"], entry["end"], 0.0) for entry in replacement_entries
426
+ (entry["start"], entry["end"], 0.0)
427
+ for entry in _original_gap_subtitle_entries(tts_segments, work_dir, video_duration)
506
428
  ]
507
429
  opaque = f"drawbox={geometry}:color=black@1.00:t=fill"
508
430
  filters.extend(