@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
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from artifacts import _load_work_json
8
8
  from audio_mix import _seg_place_window
9
9
  from lib import CONFIG
10
- from media import _load_cut_timeline_plan
10
+ from media import _plan_clip_spans
11
11
  from assemble_constants import (
12
12
  _AUTO_ORIGINAL_READ_CPS,
13
13
  _MAX_ORIGINAL_READ_CPS,
@@ -37,17 +37,15 @@ def _source_subtitle_mask_policy(work_dir=None):
37
37
  legacy flag is represented as ``legacy_implicit`` and blocks the visual gate
38
38
  instead of silently masking picture information.
39
39
  """
40
- burn = bool(CONFIG.get("burn_subtitles", False))
41
- raw_policy = str(CONFIG.get("source_subtitle_mask_policy", "") or "").strip().lower()
42
- legacy_flag = bool(CONFIG.get("mask_source_subtitles", False))
40
+ burn = CONFIG["burn_subtitles"]
41
+ raw_policy = CONFIG["source_subtitle_mask_policy"]
42
+ legacy_flag = CONFIG["mask_source_subtitles"]
43
43
  allowed = {"off", "opt_in", "safe", "forced"}
44
- declared = bool(CONFIG.get("source_subtitle_mask_policy_declared", False)) or raw_policy in {"opt_in", "safe", "forced"}
44
+ declared = CONFIG["source_subtitle_mask_policy_declared"] or raw_policy in {"opt_in", "safe", "forced"}
45
45
  implicit = False
46
46
  if legacy_flag and not declared:
47
47
  raw_policy = "legacy_implicit"
48
48
  implicit = True
49
- elif not raw_policy:
50
- raw_policy = "off"
51
49
  elif raw_policy not in allowed:
52
50
  implicit = True
53
51
  user_subtitles = _has_user_subtitles(work_dir)
@@ -78,7 +76,7 @@ def _source_subtitle_mask_policy(work_dir=None):
78
76
  "active": bool(active),
79
77
  "scope": (
80
78
  "measured_source_subtitle_band"
81
- if active and 0 <= int(CONFIG.get("subtitle_y_top", -1)) < int(CONFIG.get("subtitle_y_bot", -1))
79
+ if active and 0 <= CONFIG["subtitle_y_top"] < CONFIG["subtitle_y_bot"]
82
80
  else ("bottom_source_subtitle_band" if active else "none")
83
81
  ),
84
82
  "trigger": trigger,
@@ -86,107 +84,88 @@ def _source_subtitle_mask_policy(work_dir=None):
86
84
  "burn_subtitles": burn,
87
85
  "legacy_mask_flag": legacy_flag,
88
86
  "user_subtitles_present": user_subtitles,
89
- "blocking": bool(implicit),
87
+ "blocking": implicit,
90
88
  }
91
89
 
92
90
 
93
91
  def _load_original_asr(work_dir):
94
- """The original speech transcription (asr_result.json), SOURCE-time, cleaned to
95
- {start,end,text} with text and a positive span. [] when absent/unparseable."""
96
- segs = []
97
- for s in _load_work_json(work_dir, "asr_result.json") or []:
98
- if not isinstance(s, dict):
99
- continue
100
- try:
101
- start, end = float(s["start"]), float(s["end"])
102
- except (KeyError, TypeError, ValueError):
103
- continue
104
- text = str(s.get("text", "")).strip()
105
- if text and end > start:
106
- segs.append({"start": start, "end": end, "text": text})
107
- return segs
92
+ """The original speech transcription (asr_result.json), SOURCE-time [{start,end,text}]; [] when absent."""
93
+ data = _load_work_json(work_dir, "asr_result.json")
94
+ if data is None:
95
+ return []
96
+ return [{"start": float(s["start"]), "end": float(s["end"]), "text": s["text"]} for s in data]
108
97
 
109
98
 
110
99
  def _load_agent_original_subtitles(work_dir):
111
100
  """Agent-calibrated original-dialogue subtitles (original_subtitles.json): OUTPUT-time
112
101
  [{start,end,text}] the writer authors alongside narration.json — the corrected, gap-aligned
113
102
  transcript of what is ACTUALLY said in each original-audio gap (ASR errors/names fixed).
114
- None when absent/invalid (then assemble falls back to a conservative auto-ASR mapping)."""
103
+ None when absent or empty (then assemble falls back to a conservative auto-ASR mapping)."""
115
104
  data = _load_work_json(work_dir, "original_subtitles.json")
116
- if not isinstance(data, list):
105
+ if not data:
117
106
  return None
118
- out = []
119
- for s in data:
120
- if not isinstance(s, dict):
121
- continue
122
- try:
123
- start, end = float(s["start"]), float(s["end"])
124
- except (KeyError, TypeError, ValueError):
125
- continue
126
- text = str(s.get("text", "")).strip()
127
- if text and end > start:
128
- out.append({"start": start, "end": end, "text": text})
129
- return out or None
107
+ return [{"start": float(s["start"]), "end": float(s["end"]), "text": s["text"]} for s in data]
130
108
 
131
109
 
132
- def _clean_subtitle_segments(raw):
133
- """Coerce an iterable of {start,end,text} dicts to validated, positive-span segments."""
110
+ def _user_subtitle_entries(rows, source):
111
+ """Validate user-authored {start,end,text} rows; a malformed row is an error, not a skip."""
134
112
  out = []
135
- for s in raw or []:
136
- if not isinstance(s, dict):
137
- continue
113
+ for index, row in enumerate(rows, start=1):
138
114
  try:
139
- start, end = float(s["start"]), float(s["end"])
140
- except (KeyError, TypeError, ValueError):
141
- continue
142
- text = str(s.get("text", "")).strip()
143
- if text and end > start:
144
- out.append({"start": start, "end": end, "text": text})
115
+ start, end, text = float(row["start"]), float(row["end"]), row["text"].strip()
116
+ except (KeyError, TypeError, ValueError, AttributeError) as exc:
117
+ raise ValueError(f"{source} 第 {index} 条缺少或无法解析 start/end/text: {row!r}") from exc
118
+ if end <= start or not text:
119
+ raise ValueError(f"{source} 第 {index} 条无效(需要 end > start 且 text 非空): {row!r}")
120
+ out.append({"start": start, "end": end, "text": text})
145
121
  return out
146
122
 
147
123
 
148
124
  def _parse_srt_timestamp(value):
149
- """Parse an SRT 'HH:MM:SS,mmm' (or ASS 'H:MM:SS.cc') timestamp into seconds, or None."""
150
- m = re.match(r"\s*(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})\s*$", str(value))
125
+ """Parse an SRT 'HH:MM:SS,mmm' (or ASS 'H:MM:SS.cc') timestamp into seconds."""
126
+ m = re.match(r"\s*(\d+):(\d{1,2}):(\d{1,2})[.,](\d{1,3})\s*$", value)
151
127
  if not m:
152
- return None
128
+ raise ValueError(f"无法解析字幕时间戳: {value!r}")
153
129
  h, mm, ss, frac = m.groups()
154
130
  return int(h) * 3600 + int(mm) * 60 + int(ss) + int(frac) / (10 ** len(frac))
155
131
 
156
132
 
157
- def _parse_srt_text(text):
158
- """Minimal SRT parser → [{start,end,text}]. Tolerant of blank lines / missing indices."""
133
+ def _parse_srt_text(text, source):
134
+ """Minimal SRT parser → [{start,end,text}]. Blank lines and missing indices are tolerated;
135
+ a block without a parseable timing line is an error. Cues with no text are dropped."""
159
136
  segs = []
160
- for block in re.split(r"\n\s*\n", str(text).replace("\r\n", "\n").replace("\r", "\n")):
137
+ for block in re.split(r"\n\s*\n", text.replace("\r\n", "\n").replace("\r", "\n")):
161
138
  lines = [ln for ln in block.split("\n") if ln.strip()]
162
139
  if not lines:
163
140
  continue
164
141
  if lines[0].strip().isdigit():
165
142
  lines = lines[1:]
166
143
  if not lines or "-->" not in lines[0]:
167
- continue
168
- parts = lines[0].split("-->")
169
- if len(parts) != 2:
170
- continue
171
- start, end = _parse_srt_timestamp(parts[0]), _parse_srt_timestamp(parts[1])
144
+ raise ValueError(f"{source}: 字幕块缺少时间行: {block.strip()!r}")
145
+ start_text, end_text = lines[0].split("-->", 1)
146
+ start, end = _parse_srt_timestamp(start_text), _parse_srt_timestamp(end_text)
147
+ if end <= start:
148
+ raise ValueError(f"{source}: 字幕结束时间必须晚于开始时间: {lines[0].strip()!r}")
172
149
  body = " ".join(lines[1:]).strip()
173
- if start is not None and end is not None and end > start and body:
150
+ if body:
174
151
  segs.append({"start": start, "end": end, "text": body})
175
152
  return segs
176
153
 
177
154
 
178
- def _parse_ass_text(text):
155
+ def _parse_ass_text(text, source):
179
156
  """Minimal ASS Dialogue parser → [{start,end,text}] (Start, End are fields 2 and 3)."""
180
157
  segs = []
181
- for line in str(text).replace("\r\n", "\n").replace("\r", "\n").split("\n"):
158
+ for line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
182
159
  if not line.startswith("Dialogue:"):
183
160
  continue
184
161
  fields = line[len("Dialogue:"):].split(",", 9)
185
162
  if len(fields) < 10:
186
- continue
163
+ raise ValueError(f"{source}: Dialogue 行字段不足: {line!r}")
187
164
  start, end = _parse_srt_timestamp(fields[1]), _parse_srt_timestamp(fields[2])
165
+ if end <= start:
166
+ raise ValueError(f"{source}: 字幕结束时间必须晚于开始时间: {line!r}")
188
167
  body = re.sub(r"\{[^}]*\}", "", fields[9]).replace("\\N", " ").replace("\\n", " ").strip()
189
- if start is not None and end is not None and end > start and body:
168
+ if body:
190
169
  segs.append({"start": start, "end": end, "text": body})
191
170
  return segs
192
171
 
@@ -200,79 +179,39 @@ def _load_user_original_subtitles(work_dir):
200
179
  via the cut clip spans, "output" (default) is used directly.
201
180
  - user_subtitles.srt / user_subtitles.ass: parsed minimally and defaulted to SOURCE-time,
202
181
  so they are remapped to OUTPUT via the cut clip spans.
203
- Returns OUTPUT-time [{start,end,text}], or None when absent/malformed (caller falls back)."""
182
+ Returns OUTPUT-time [{start,end,text}], or None when no user file exists. A malformed
183
+ file raises: the user asked for these subtitles, so silently falling back is wrong."""
204
184
  work = Path(work_dir)
205
185
  json_path = work / "user_subtitles.json"
206
186
  if json_path.exists():
207
187
  try:
208
188
  data = json.loads(json_path.read_text(encoding="utf-8"))
209
- except (ValueError, OSError):
210
- return None
211
- if isinstance(data, list):
212
- segs, timeline = _clean_subtitle_segments(data), "output"
213
- elif isinstance(data, dict):
214
- segs = _clean_subtitle_segments(data.get("lines"))
215
- timeline = str(data.get("timeline", "output")).lower()
189
+ except ValueError as exc:
190
+ raise ValueError(f"{json_path} 不是合法 JSON: {exc}") from exc
191
+ if isinstance(data, dict):
192
+ timeline = data.get("timeline", "output")
193
+ if timeline not in {"source", "output"}:
194
+ raise ValueError(f"{json_path}: timeline 必须是 source 或 output,当前为 {timeline!r}")
195
+ rows = data["lines"]
216
196
  else:
217
- return None
218
- if not segs:
219
- return None
197
+ timeline, rows = "output", data
198
+ segs = _user_subtitle_entries(rows, json_path.name)
220
199
  if timeline == "source":
221
- segs = _map_asr_to_output(segs, _output_clip_spans(work))
222
- return segs or None
200
+ segs = _map_asr_to_output(segs, _plan_clip_spans(work))
201
+ return segs
223
202
 
224
203
  for name in ("user_subtitles.srt", "user_subtitles.ass"):
225
204
  path = work / name
226
205
  if not path.exists():
227
206
  continue
228
- try:
229
- text = path.read_text(encoding="utf-8")
230
- except OSError:
231
- return None
232
207
  parser = _parse_ass_text if name.endswith(".ass") else _parse_srt_text
233
- segs = _clean_subtitle_segments(parser(text))
234
- if not segs:
235
- return None
208
+ segs = parser(path.read_text(encoding="utf-8"), name)
236
209
  # .srt/.ass default to SOURCE-time → remap onto the output timeline (identity in full mode).
237
- segs = _map_asr_to_output(segs, _output_clip_spans(work))
238
- return segs or None
210
+ return _map_asr_to_output(segs, _plan_clip_spans(work))
239
211
 
240
212
  return None
241
213
 
242
214
 
243
- def _output_clip_spans(work_dir):
244
- """Cut-mode source→output clip spans using the same freshness logic as video clips."""
245
- try:
246
- plan = _load_cut_timeline_plan(work_dir)
247
- except (OSError, ValueError, json.JSONDecodeError):
248
- plan = None
249
- if not plan:
250
- return None
251
- entries = plan.get("clips", plan) if isinstance(plan, dict) else plan
252
- if not isinstance(entries, list):
253
- return None
254
- spans, cursor = [], 0.0
255
- for c in entries:
256
- if not isinstance(c, dict):
257
- continue
258
- try:
259
- ss = float(c.get("source_start", c.get("start")))
260
- se = float(c.get("source_end", c.get("end")))
261
- except (TypeError, ValueError):
262
- continue
263
- if se - ss <= 0:
264
- continue
265
- out_s, out_e = c.get("output_start"), c.get("output_end")
266
- if out_s is None or out_e is None:
267
- out_s, out_e = cursor, cursor + (se - ss)
268
- cursor += se - ss
269
- else:
270
- out_s, out_e = float(out_s), float(out_e)
271
- cursor = max(cursor, out_e)
272
- spans.append({"source_start": ss, "source_end": se, "output_start": out_s, "output_end": out_e})
273
- return spans or None
274
-
275
-
276
215
  def _map_asr_to_output(asr_segs, clip_spans):
277
216
  """Map SOURCE-time ASR segments onto the OUTPUT timeline. Full mode (clip_spans None) is
278
217
  identity; cut mode intersects each ASR span with each kept clip (a straddling line yields one
@@ -296,8 +235,7 @@ def _map_asr_to_output(asr_segs, clip_spans):
296
235
  def _narration_gap_windows(tts_segments, video_duration, min_gap=_MIN_GAP_TO_SUBTITLE):
297
236
  """OUTPUT-timeline stretches with NO narration (the original-audio blocks): the complement of
298
237
  the merged narration placement windows within [0, video_duration], keeping gaps >= min_gap."""
299
- placed = sorted(
300
- (_seg_place_window(s) for s in tts_segments if isinstance(s, dict)), key=lambda w: w[0])
238
+ placed = sorted(map(_seg_place_window, tts_segments), key=lambda w: w[0])
301
239
  merged = []
302
240
  for s, e in placed:
303
241
  if e - s <= 0:
@@ -325,10 +263,9 @@ def _original_gap_subtitle_entries(tts_segments, work_dir, video_duration):
325
263
  # the original dialogue shown, e.g. a clean/foreign source with mask OFF (no burned subs to
326
264
  # double). Without a user file we keep the mask requirement so we don't double the source's own
327
265
  # visible subs. subtitle_original_in_gaps is the explicit override either way.
328
- mask_covers_gaps = _source_subtitle_mask_covers_gaps(work_dir)
329
- if not (CONFIG.get("burn_subtitles", False)
330
- and CONFIG.get("subtitle_original_in_gaps", True)
331
- and (mask_covers_gaps or _has_user_subtitles(work_dir))):
266
+ if not (CONFIG["burn_subtitles"]
267
+ and CONFIG["subtitle_original_in_gaps"]
268
+ and (_source_subtitle_mask_covers_gaps(work_dir) or _has_user_subtitles(work_dir))):
332
269
  return []
333
270
  gaps = _narration_gap_windows(tts_segments, video_duration)
334
271
  if not gaps:
@@ -338,23 +275,17 @@ def _original_gap_subtitle_entries(tts_segments, work_dir, video_duration):
338
275
  # conservative auto-ASR mapping. The user file and the agent file are time-precise (their spans
339
276
  # are the real on-screen windows), so they take the interval-clip "precise" path; raw ASR is
340
277
  # coarse and stays on the midpoint+over-render-guard fallback path.
278
+ max_chars = CONFIG["subtitle_max_chars"]
341
279
  user = _load_user_original_subtitles(work_dir)
342
280
  if user is not None:
343
- candidates, precise = user, True
344
- else:
345
- agent = _load_agent_original_subtitles(work_dir)
346
- if agent is not None:
347
- candidates, precise = agent, True
348
- else:
349
- asr = _load_original_asr(work_dir)
350
- if not asr:
351
- return []
352
- candidates, precise = _map_asr_to_output(asr, _output_clip_spans(work_dir)), False
353
-
354
- max_chars = int(CONFIG.get("subtitle_max_chars", 20))
355
- if precise:
356
- return _precise_gap_entries(candidates, gaps, max_chars)
357
- return _fallback_gap_entries(candidates, gaps, max_chars)
281
+ return _precise_gap_entries(user, gaps, max_chars)
282
+ agent = _load_agent_original_subtitles(work_dir)
283
+ if agent is not None:
284
+ return _precise_gap_entries(agent, gaps, max_chars)
285
+ asr = _load_original_asr(work_dir)
286
+ if not asr:
287
+ return []
288
+ return _fallback_gap_entries(_map_asr_to_output(asr, _plan_clip_spans(work_dir)), gaps, max_chars)
358
289
 
359
290
 
360
291
  def _precise_gap_entries(candidates, gaps, max_chars):
@@ -364,25 +295,23 @@ def _precise_gap_entries(candidates, gaps, max_chars):
364
295
  shorter than _MIN_READABLE_SECONDS are dropped. No over-render guard (the source is trusted)."""
365
296
  entries = []
366
297
  for seg in candidates:
367
- text = str(seg["text"]).strip()
368
- if not text:
369
- continue
298
+ text = seg["text"]
370
299
  seg_start, seg_end = float(seg["start"]), float(seg["end"])
371
- seg_dur = seg_end - seg_start
372
- overlaps = []
373
- for gs, ge in gaps:
374
- cs, ce = max(seg_start, gs), min(seg_end, ge)
375
- if ce - cs >= _MIN_READABLE_SECONDS:
376
- overlaps.append((cs, ce))
300
+ overlaps = [
301
+ (max(seg_start, gs), min(seg_end, ge))
302
+ for gs, ge in gaps
303
+ if min(seg_end, ge) - max(seg_start, gs) >= _MIN_READABLE_SECONDS
304
+ ]
377
305
  if not overlaps:
378
306
  continue
379
- if len(overlaps) == 1 or seg_dur <= 0:
307
+ if len(overlaps) == 1:
380
308
  # the common case (a line authored within one gap): show it whole in that gap
381
309
  cs, ce = overlaps[0]
382
310
  entries.extend(_bracketed_original_chunks(text, cs, ce, max_chars))
383
311
  continue
384
312
  # the line straddles a narration block: show each gap only ITS portion of the text
385
313
  # (proportional to the time the line overlaps that gap) instead of the whole line twice.
314
+ seg_dur = seg_end - seg_start
386
315
  n = len(text)
387
316
  for cs, ce in overlaps:
388
317
  lo = max(0, int(round((cs - seg_start) / seg_dur * n)))
@@ -397,7 +326,7 @@ def _split_sentences_keep_delims(text):
397
326
  """Split on terminal CJK sentence marks 。!? keeping each delimiter with its sentence. A
398
327
  fragment that is only closing quotes/brackets (e.g. a trailing 」 after a 。 inside a quote) is
399
328
  re-attached to the previous sentence so quoted speech is never split off into a bare 」."""
400
- parts = [p.strip() for p in re.split(r"(?<=[。!?])", str(text)) if p.strip()]
329
+ parts = [p.strip() for p in re.split(r"(?<=[。!?])", text) if p.strip()]
401
330
  merged = []
402
331
  for part in parts:
403
332
  if merged and all(ch in _SUBTITLE_CLOSING_QUOTES for ch in part):
@@ -418,11 +347,8 @@ def _fallback_gap_entries(candidates, gaps, max_chars):
418
347
  # its char-proportional midpoint lands in (the only "which gap" signal coarse ASR gives).
419
348
  buckets = {} # gap_index -> [(estimated_onset, sentence_text)]
420
349
  for seg in candidates:
421
- for sentence in _split_sentences_keep_delims(seg["text"]) or [str(seg["text"]).strip()]:
422
- text = sentence.strip()
423
- if not text:
424
- continue
425
- sub = _sentence_subspan(seg, sentence)
350
+ for text in _split_sentences_keep_delims(seg["text"]):
351
+ sub = _sentence_subspan(seg, text)
426
352
  mid = (sub["start"] + sub["end"]) / 2.0
427
353
  gi = next((i for i, (gs, ge) in enumerate(gaps) if gs <= mid < ge), None)
428
354
  if gi is None:
@@ -447,8 +373,7 @@ def _fallback_gap_entries(candidates, gaps, max_chars):
447
373
  if ce2 - cursor < _MIN_READABLE_SECONDS:
448
374
  break
449
375
  max_len = int((ce2 - cursor) * _MAX_ORIGINAL_READ_CPS)
450
- shown = text if len(text) <= max_len else text[:max_len]
451
- entries.extend(_bracketed_original_chunks(shown, cursor, ce2, max_chars))
376
+ entries.extend(_bracketed_original_chunks(text[:max_len], cursor, ce2, max_chars))
452
377
  cursor = ce2
453
378
  return entries
454
379
 
@@ -456,22 +381,20 @@ def _fallback_gap_entries(candidates, gaps, max_chars):
456
381
  def _sentence_subspan(seg, sentence):
457
382
  """The slice of seg's [start,end] window that this sentence occupies, by character proportion.
458
383
  Single-sentence lines return the whole span unchanged."""
459
- full = str(seg["text"]).strip()
460
- if not full or sentence.strip() == full:
461
- return {"start": seg["start"], "end": seg["end"]}
462
- idx = full.find(sentence.strip())
463
- if idx < 0:
384
+ full = seg["text"].strip()
385
+ idx = full.find(sentence)
386
+ if not full or sentence == full or idx < 0:
464
387
  return {"start": seg["start"], "end": seg["end"]}
465
388
  span = seg["end"] - seg["start"]
466
389
  s = seg["start"] + span * (idx / len(full))
467
- e = seg["start"] + span * ((idx + len(sentence.strip())) / len(full))
390
+ e = seg["start"] + span * ((idx + len(sentence)) / len(full))
468
391
  return {"start": s, "end": e}
469
392
 
470
393
 
471
394
  def _combined_subtitle_entries(narration, work_dir, video_duration):
472
395
  """Narration subtitle entries plus original-dialogue entries in the gaps, sorted by start.
473
396
  Original entries are confined to narration gaps, so they never overlap narration entries."""
474
- entries = list(_subtitle_entries(narration))
397
+ entries = _subtitle_entries(narration)
475
398
  entries.extend(_original_gap_subtitle_entries(narration, work_dir, video_duration))
476
399
  entries.sort(key=lambda x: (x["start"], x["end"]))
477
400
  return entries
@@ -479,8 +402,6 @@ def _combined_subtitle_entries(narration, work_dir, video_duration):
479
402
 
480
403
  def _source_subtitle_mask_covers_gaps(work_dir=None):
481
404
  """Whether the effective source mask hides hardcoded subtitles outside narration."""
482
- if not _source_subtitle_mask_policy(work_dir).get("active"):
405
+ if not _source_subtitle_mask_policy(work_dir)["active"]:
483
406
  return False
484
- opacity = max(0.0, min(1.0, float(CONFIG.get("subtitle_mask_opacity", 0.6))))
485
- timing = str(CONFIG.get("source_subtitle_mask_timing", "narration") or "narration").lower()
486
- return opacity >= 1.0 - 1e-9 and timing == "all"
407
+ return CONFIG["subtitle_mask_opacity"] >= 1.0 - 1e-9 and CONFIG["source_subtitle_mask_timing"] == "all"