@oh-story/dsh 0.1.4 → 0.1.6

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 (479) hide show
  1. package/README.md +56 -12
  2. package/lib/client.js +2707 -268
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +136 -101
  5. package/lib/drama/skills/short-drama/SKILL.md +61 -1
  6. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
  8. package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
  9. package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
  10. package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
  11. package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
  12. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  13. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  14. package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
  15. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  16. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
  17. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
  18. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  19. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
  21. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
  22. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
  23. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
  24. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
  25. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  26. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  27. package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
  28. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  29. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  30. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  31. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  32. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  33. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  34. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
  35. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  36. package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
  37. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  38. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
  39. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  40. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
  41. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
  42. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
  43. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  44. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
  45. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  46. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  47. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
  50. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
  51. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  53. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
  54. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  55. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  56. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  57. package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
  58. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  59. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  60. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  61. package/lib/index.js +865 -33
  62. package/lib/index.js.map +4 -4
  63. package/lib/novel-to-game/LICENSE +21 -0
  64. package/lib/novel-to-game/README.md +180 -0
  65. package/lib/novel-to-game/README_ZH.md +178 -0
  66. package/lib/novel-to-game/VERSION +1 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  91. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  92. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  93. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  94. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  95. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  96. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  97. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  98. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  99. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  100. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  101. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  102. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  103. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  104. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  128. package/lib/novel-to-game/manifest.json +508 -0
  129. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  130. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  131. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  132. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  133. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  134. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  135. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  136. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  137. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  138. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  139. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  140. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  141. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  142. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  143. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  144. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  145. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  146. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  147. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  148. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  149. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  150. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  151. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  152. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  153. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  154. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  155. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  156. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  157. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  158. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  159. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  160. package/lib/oh-story/manifest.json +415 -305
  161. package/lib/oh-story/roles/consistency-checker.md +1 -1
  162. package/lib/oh-story/roles/narrative-writer.md +32 -33
  163. package/lib/oh-story/roles/story-architect.md +16 -26
  164. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  165. package/lib/oh-story/skills/story/SKILL.md +16 -9
  166. package/lib/oh-story/skills/story/VERSION +1 -1
  167. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  168. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  169. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  170. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  171. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  172. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  173. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  174. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  175. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  176. package/lib/oh-story/skills/story-import/SKILL.md +19 -10
  177. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  180. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  181. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  182. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  183. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  184. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  185. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  186. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  187. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  188. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  189. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  190. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  191. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  192. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  193. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  194. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  195. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  196. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  197. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  198. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
  199. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
  200. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  201. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  202. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  203. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  204. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  205. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  206. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
  207. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  208. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
  209. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  210. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  211. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  212. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  213. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  214. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  215. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  216. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  217. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  218. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  219. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  220. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  221. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  222. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  223. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  224. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  225. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  226. package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
  227. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  228. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  229. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  230. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  236. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  237. package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
  238. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  239. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  243. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  244. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  245. package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
  246. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  247. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  248. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  249. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  250. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  251. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  252. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  253. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  254. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  255. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  257. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  258. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  259. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  260. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  261. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  262. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  263. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  264. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  265. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  266. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  267. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  268. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  270. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  271. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  272. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  273. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  274. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  276. 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
  277. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
  278. 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
  279. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  280. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
  281. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
  282. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  283. 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
  284. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  285. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  286. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  287. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  288. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  289. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  290. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  291. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  292. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  293. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  294. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  295. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  296. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  297. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  298. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  299. package/lib/types/client/drama-production-view.d.ts +34 -0
  300. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  301. package/lib/types/client/drama-production.d.ts +92 -0
  302. package/lib/types/client/drama-production.d.ts.map +1 -0
  303. package/lib/types/client/file-activity.d.ts +4 -2
  304. package/lib/types/client/file-activity.d.ts.map +1 -1
  305. package/lib/types/client/index.d.ts +1 -1
  306. package/lib/types/client/index.d.ts.map +1 -1
  307. package/lib/types/client/production-intents.d.ts +9 -0
  308. package/lib/types/client/production-intents.d.ts.map +1 -0
  309. package/lib/types/client/production-prompts.d.ts +9 -0
  310. package/lib/types/client/production-prompts.d.ts.map +1 -0
  311. package/lib/types/client/production-runtime.d.ts +58 -0
  312. package/lib/types/client/production-runtime.d.ts.map +1 -0
  313. package/lib/types/client/video-studio.d.ts +41 -0
  314. package/lib/types/client/video-studio.d.ts.map +1 -0
  315. package/lib/types/client/workbench-ui.d.ts +5 -0
  316. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  317. package/lib/types/game-verification.d.ts +16 -0
  318. package/lib/types/game-verification.d.ts.map +1 -0
  319. package/lib/types/index.d.ts +3 -1
  320. package/lib/types/index.d.ts.map +1 -1
  321. package/lib/types/production-intent.d.ts +21 -0
  322. package/lib/types/production-intent.d.ts.map +1 -0
  323. package/lib/types/production-tool.d.ts +5 -0
  324. package/lib/types/production-tool.d.ts.map +1 -0
  325. package/lib/types/role-tool.d.ts.map +1 -1
  326. package/lib/types/skill-provider.d.ts +7 -0
  327. package/lib/types/skill-provider.d.ts.map +1 -1
  328. package/lib/types/video-project.d.ts +42 -0
  329. package/lib/types/video-project.d.ts.map +1 -0
  330. package/lib/types/workspace-request-trust.d.ts +6 -0
  331. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  332. package/lib/types/workspace-route.d.ts +20 -0
  333. package/lib/types/workspace-route.d.ts.map +1 -1
  334. package/lib/video-recap/CHANGELOG.md +282 -0
  335. package/lib/video-recap/LICENSE +21 -0
  336. package/lib/video-recap/README.en.md +257 -0
  337. package/lib/video-recap/README.md +255 -0
  338. package/lib/video-recap/manifest.json +684 -0
  339. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  340. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  341. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  342. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  343. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  344. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  345. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  346. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  347. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  348. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  349. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  350. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  351. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  352. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  353. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  354. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  355. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  356. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  357. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  358. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  359. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  360. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  361. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  362. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  363. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  364. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  365. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  366. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  367. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  368. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  369. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  370. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  371. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  372. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  373. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  374. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  375. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  376. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  377. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  378. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  379. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  380. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  381. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  382. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  383. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  384. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  385. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  386. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  387. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  388. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  389. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  390. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  391. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  392. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  393. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  394. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  395. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  396. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  397. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  398. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  399. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  400. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  401. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  402. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  403. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  404. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  405. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  406. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  407. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  408. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  409. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  410. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  411. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  412. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  413. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  414. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  415. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  416. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  417. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  418. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  419. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  420. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  421. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  422. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  423. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  424. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  425. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  426. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  427. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  428. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  429. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  430. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  431. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  432. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  433. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  434. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  435. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  436. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  437. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  438. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  439. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  440. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  441. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  442. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  443. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  444. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  445. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  446. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  447. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  448. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  449. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  450. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  451. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  452. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  453. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  454. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  455. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  456. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  457. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  458. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  459. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  460. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  461. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  462. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  463. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  464. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  465. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  466. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  467. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  468. package/package.json +55 -40
  469. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  470. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  471. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  472. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  473. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  474. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  475. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  476. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  477. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  478. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  479. /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
@@ -0,0 +1,486 @@
1
+ """Original-dialogue subtitle loading, source mapping, and gap placement."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from artifacts import _load_work_json
8
+ from audio_mix import _seg_place_window
9
+ from lib import CONFIG
10
+ from media import _load_cut_timeline_plan
11
+ from assemble_constants import (
12
+ _AUTO_ORIGINAL_READ_CPS,
13
+ _MAX_ORIGINAL_READ_CPS,
14
+ _MIN_ASR_CLIP_OVERLAP,
15
+ _MIN_GAP_TO_SUBTITLE,
16
+ _MIN_READABLE_SECONDS,
17
+ _SUBTITLE_CLOSING_QUOTES,
18
+ )
19
+ from subtitle_core import (
20
+ _bracketed_original_chunks,
21
+ _subtitle_entries,
22
+ )
23
+
24
+ def _has_user_subtitles(work_dir):
25
+ """True when the user dropped a bring-your-own original-subtitle file into work_dir."""
26
+ return work_dir is not None and any(
27
+ (Path(work_dir) / name).exists()
28
+ for name in ("user_subtitles.json", "user_subtitles.srt", "user_subtitles.ass")
29
+ )
30
+
31
+
32
+ def _source_subtitle_mask_policy(work_dir=None):
33
+ """Explicit source-subtitle mask policy and trigger facts for visual QC/cache keys.
34
+
35
+ Older builds treated ``MASK_SOURCE_SUBTITLES=True`` as an ambient default black
36
+ band. The visual contract now requires an explicit policy, so a bare truthy
37
+ legacy flag is represented as ``legacy_implicit`` and blocks the visual gate
38
+ instead of silently masking picture information.
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))
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"}
45
+ implicit = False
46
+ if legacy_flag and not declared:
47
+ raw_policy = "legacy_implicit"
48
+ implicit = True
49
+ elif not raw_policy:
50
+ raw_policy = "off"
51
+ elif raw_policy not in allowed:
52
+ implicit = True
53
+ user_subtitles = _has_user_subtitles(work_dir)
54
+ active = False
55
+ trigger = "policy_off"
56
+ reason = "source subtitle masking disabled by explicit policy"
57
+ if raw_policy == "off":
58
+ active = False
59
+ elif raw_policy in {"opt_in", "forced"}:
60
+ active = burn and legacy_flag
61
+ trigger = "burn_subtitles_and_legacy_mask_flag"
62
+ reason = "explicit policy permits masking only with burned recap subtitles"
63
+ elif raw_policy == "safe":
64
+ active = burn and (legacy_flag or user_subtitles)
65
+ trigger = "safe_policy_with_burned_subtitles"
66
+ reason = "safe policy masks only when recap subtitles are burned and an original-subtitle source is declared"
67
+ else:
68
+ active = False
69
+ trigger = "implicit_or_invalid_policy"
70
+ reason = "mask_source_subtitles requires explicit SOURCE_SUBTITLE_MASK_POLICY"
71
+ if not burn and active:
72
+ active = False
73
+ trigger = "burn_subtitles_disabled"
74
+ reason = "mask-only black band is forbidden without burned recap subtitles"
75
+ return {
76
+ "policy": raw_policy,
77
+ "declared": bool(declared and raw_policy in allowed),
78
+ "active": bool(active),
79
+ "scope": (
80
+ "measured_source_subtitle_band"
81
+ if active and 0 <= int(CONFIG.get("subtitle_y_top", -1)) < int(CONFIG.get("subtitle_y_bot", -1))
82
+ else ("bottom_source_subtitle_band" if active else "none")
83
+ ),
84
+ "trigger": trigger,
85
+ "reason": reason,
86
+ "burn_subtitles": burn,
87
+ "legacy_mask_flag": legacy_flag,
88
+ "user_subtitles_present": user_subtitles,
89
+ "blocking": bool(implicit),
90
+ }
91
+
92
+
93
+ 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
108
+
109
+
110
+ def _load_agent_original_subtitles(work_dir):
111
+ """Agent-calibrated original-dialogue subtitles (original_subtitles.json): OUTPUT-time
112
+ [{start,end,text}] the writer authors alongside narration.json — the corrected, gap-aligned
113
+ 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)."""
115
+ data = _load_work_json(work_dir, "original_subtitles.json")
116
+ if not isinstance(data, list):
117
+ 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
130
+
131
+
132
+ def _clean_subtitle_segments(raw):
133
+ """Coerce an iterable of {start,end,text} dicts to validated, positive-span segments."""
134
+ out = []
135
+ for s in raw or []:
136
+ if not isinstance(s, dict):
137
+ continue
138
+ 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})
145
+ return out
146
+
147
+
148
+ 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))
151
+ if not m:
152
+ return None
153
+ h, mm, ss, frac = m.groups()
154
+ return int(h) * 3600 + int(mm) * 60 + int(ss) + int(frac) / (10 ** len(frac))
155
+
156
+
157
+ def _parse_srt_text(text):
158
+ """Minimal SRT parser → [{start,end,text}]. Tolerant of blank lines / missing indices."""
159
+ segs = []
160
+ for block in re.split(r"\n\s*\n", str(text).replace("\r\n", "\n").replace("\r", "\n")):
161
+ lines = [ln for ln in block.split("\n") if ln.strip()]
162
+ if not lines:
163
+ continue
164
+ if lines[0].strip().isdigit():
165
+ lines = lines[1:]
166
+ 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])
172
+ body = " ".join(lines[1:]).strip()
173
+ if start is not None and end is not None and end > start and body:
174
+ segs.append({"start": start, "end": end, "text": body})
175
+ return segs
176
+
177
+
178
+ def _parse_ass_text(text):
179
+ """Minimal ASS Dialogue parser → [{start,end,text}] (Start, End are fields 2 and 3)."""
180
+ segs = []
181
+ for line in str(text).replace("\r\n", "\n").replace("\r", "\n").split("\n"):
182
+ if not line.startswith("Dialogue:"):
183
+ continue
184
+ fields = line[len("Dialogue:"):].split(",", 9)
185
+ if len(fields) < 10:
186
+ continue
187
+ start, end = _parse_srt_timestamp(fields[1]), _parse_srt_timestamp(fields[2])
188
+ 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:
190
+ segs.append({"start": start, "end": end, "text": body})
191
+ return segs
192
+
193
+
194
+ def _load_user_original_subtitles(work_dir):
195
+ """User-supplied original-dialogue subtitles, the highest-priority source (above the agent file).
196
+
197
+ Accepts (first existing wins):
198
+ - user_subtitles.json: a bare list [{start,end,text}] (treated as OUTPUT-time, used verbatim),
199
+ OR a wrapper {"timeline":"source"|"output", "lines":[...]} — "source" is remapped to OUTPUT
200
+ via the cut clip spans, "output" (default) is used directly.
201
+ - user_subtitles.srt / user_subtitles.ass: parsed minimally and defaulted to SOURCE-time,
202
+ 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)."""
204
+ work = Path(work_dir)
205
+ json_path = work / "user_subtitles.json"
206
+ if json_path.exists():
207
+ try:
208
+ 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()
216
+ else:
217
+ return None
218
+ if not segs:
219
+ return None
220
+ if timeline == "source":
221
+ segs = _map_asr_to_output(segs, _output_clip_spans(work))
222
+ return segs or None
223
+
224
+ for name in ("user_subtitles.srt", "user_subtitles.ass"):
225
+ path = work / name
226
+ if not path.exists():
227
+ continue
228
+ try:
229
+ text = path.read_text(encoding="utf-8")
230
+ except OSError:
231
+ return None
232
+ 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
236
+ # .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
239
+
240
+ return None
241
+
242
+
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
+ def _map_asr_to_output(asr_segs, clip_spans):
277
+ """Map SOURCE-time ASR segments onto the OUTPUT timeline. Full mode (clip_spans None) is
278
+ identity; cut mode intersects each ASR span with each kept clip (a straddling line yields one
279
+ fragment per clip; lines in cut-away footage are dropped)."""
280
+ if clip_spans is None:
281
+ return [dict(s) for s in asr_segs]
282
+ out = []
283
+ for seg in asr_segs:
284
+ for c in clip_spans:
285
+ ov_s, ov_e = max(seg["start"], c["source_start"]), min(seg["end"], c["source_end"])
286
+ if ov_e - ov_s <= _MIN_ASR_CLIP_OVERLAP:
287
+ continue
288
+ out.append({
289
+ "start": c["output_start"] + (ov_s - c["source_start"]),
290
+ "end": c["output_start"] + (ov_e - c["source_start"]),
291
+ "text": seg["text"],
292
+ })
293
+ return out
294
+
295
+
296
+ def _narration_gap_windows(tts_segments, video_duration, min_gap=_MIN_GAP_TO_SUBTITLE):
297
+ """OUTPUT-timeline stretches with NO narration (the original-audio blocks): the complement of
298
+ 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])
301
+ merged = []
302
+ for s, e in placed:
303
+ if e - s <= 0:
304
+ continue
305
+ if merged and s <= merged[-1][1]:
306
+ merged[-1][1] = max(merged[-1][1], e)
307
+ else:
308
+ merged.append([s, e])
309
+ gaps, cursor = [], 0.0
310
+ for s, e in merged:
311
+ if s - cursor >= min_gap:
312
+ gaps.append((cursor, s))
313
+ cursor = max(cursor, e)
314
+ if video_duration - cursor >= min_gap:
315
+ gaps.append((cursor, float(video_duration)))
316
+ return gaps
317
+
318
+
319
+ def _original_gap_subtitle_entries(tts_segments, work_dir, video_duration):
320
+ """Subtitle entries for the ORIGINAL dialogue during the original-audio blocks (narration
321
+ gaps), so the band is not blank while the original speaks. Off unless we are burning and
322
+ subtitle_original_in_gaps is set; no-op when there is no ASR. Cut mode remaps ASR to output."""
323
+ # Fill the gaps when either (a) we are masking the source's own burned-in subs (so the band is
324
+ # blank without us), or (b) the user supplied their own subtitle file — a clear signal they want
325
+ # the original dialogue shown, e.g. a clean/foreign source with mask OFF (no burned subs to
326
+ # double). Without a user file we keep the mask requirement so we don't double the source's own
327
+ # 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))):
332
+ return []
333
+ gaps = _narration_gap_windows(tts_segments, video_duration)
334
+ if not gaps:
335
+ return []
336
+
337
+ # Source ladder (highest priority first): user-supplied file → agent-calibrated transcript →
338
+ # conservative auto-ASR mapping. The user file and the agent file are time-precise (their spans
339
+ # are the real on-screen windows), so they take the interval-clip "precise" path; raw ASR is
340
+ # coarse and stays on the midpoint+over-render-guard fallback path.
341
+ user = _load_user_original_subtitles(work_dir)
342
+ 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)
358
+
359
+
360
+ def _precise_gap_entries(candidates, gaps, max_chars):
361
+ """Precise path for time-accurate sources (user / agent-calibrated): interval-CLIP each line
362
+ across the gap boundaries it overlaps, emitting one sub-entry per overlapped gap (clipped to
363
+ that gap). A line straddling two gaps is split, not snapped to one or dropped; only sub-fragments
364
+ shorter than _MIN_READABLE_SECONDS are dropped. No over-render guard (the source is trusted)."""
365
+ entries = []
366
+ for seg in candidates:
367
+ text = str(seg["text"]).strip()
368
+ if not text:
369
+ continue
370
+ 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))
377
+ if not overlaps:
378
+ continue
379
+ if len(overlaps) == 1 or seg_dur <= 0:
380
+ # the common case (a line authored within one gap): show it whole in that gap
381
+ cs, ce = overlaps[0]
382
+ entries.extend(_bracketed_original_chunks(text, cs, ce, max_chars))
383
+ continue
384
+ # the line straddles a narration block: show each gap only ITS portion of the text
385
+ # (proportional to the time the line overlaps that gap) instead of the whole line twice.
386
+ n = len(text)
387
+ for cs, ce in overlaps:
388
+ lo = max(0, int(round((cs - seg_start) / seg_dur * n)))
389
+ hi = min(n, int(round((ce - seg_start) / seg_dur * n)))
390
+ piece = text[lo:hi].strip()
391
+ if piece:
392
+ entries.extend(_bracketed_original_chunks(piece, cs, ce, max_chars))
393
+ return entries
394
+
395
+
396
+ def _split_sentences_keep_delims(text):
397
+ """Split on terminal CJK sentence marks 。!? keeping each delimiter with its sentence. A
398
+ fragment that is only closing quotes/brackets (e.g. a trailing 」 after a 。 inside a quote) is
399
+ 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()]
401
+ merged = []
402
+ for part in parts:
403
+ if merged and all(ch in _SUBTITLE_CLOSING_QUOTES for ch in part):
404
+ merged[-1] += part
405
+ else:
406
+ merged.append(part)
407
+ return merged
408
+
409
+
410
+ def _fallback_gap_entries(candidates, gaps, max_chars):
411
+ """Coarse-ASR fallback. Each coarse-ASR line spans a whole window with no per-sentence onset, so
412
+ it is split into WHOLE sentences (never mid-word); each sentence is assigned to the gap its
413
+ char-proportional midpoint lands in, and within a gap the assigned sentences are packed
414
+ SEQUENTIALLY from the first one's estimated onset at a comfortable read rate — so two lines in
415
+ one gap never overlap or scatter to char-proportional tail slots — capped at the gap end. An
416
+ over-dense gap front-truncates (shown) rather than dropping to blank."""
417
+ # 1) split each coarse line into WHOLE sentences (never mid-word) and assign each to the gap
418
+ # its char-proportional midpoint lands in (the only "which gap" signal coarse ASR gives).
419
+ buckets = {} # gap_index -> [(estimated_onset, sentence_text)]
420
+ 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)
426
+ mid = (sub["start"] + sub["end"]) / 2.0
427
+ gi = next((i for i, (gs, ge) in enumerate(gaps) if gs <= mid < ge), None)
428
+ if gi is None:
429
+ continue
430
+ buckets.setdefault(gi, []).append((sub["start"], text))
431
+ # 2) within each gap, pack the assigned sentences SEQUENTIALLY from the gap onset at a
432
+ # comfortable read rate. Anchoring to the gap onset (vs each sentence's char-proportional
433
+ # tail position) stops a line heard early from being shoved to the END of its window — the
434
+ # coarse-ASR lag. Full mode keeps the real ASR onset (the first sentence's own start); an
435
+ # over-dense gap front-truncates (shown) rather than dropping to blank.
436
+ entries = []
437
+ for gi, items in buckets.items():
438
+ gs, ge = gaps[gi]
439
+ items.sort(key=lambda it: it[0])
440
+ # start at the first assigned sentence's estimated onset (clamped into the gap), then pack
441
+ # the rest sequentially so they never overlap or scatter to char-proportional tail slots.
442
+ cursor = min(ge, max(gs, min(start for start, _ in items)))
443
+ for _, text in items:
444
+ if cursor >= ge - _MIN_READABLE_SECONDS:
445
+ break
446
+ ce2 = min(ge, cursor + max(_MIN_READABLE_SECONDS, len(text) / _AUTO_ORIGINAL_READ_CPS))
447
+ if ce2 - cursor < _MIN_READABLE_SECONDS:
448
+ break
449
+ 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))
452
+ cursor = ce2
453
+ return entries
454
+
455
+
456
+ def _sentence_subspan(seg, sentence):
457
+ """The slice of seg's [start,end] window that this sentence occupies, by character proportion.
458
+ 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:
464
+ return {"start": seg["start"], "end": seg["end"]}
465
+ span = seg["end"] - seg["start"]
466
+ s = seg["start"] + span * (idx / len(full))
467
+ e = seg["start"] + span * ((idx + len(sentence.strip())) / len(full))
468
+ return {"start": s, "end": e}
469
+
470
+
471
+ def _combined_subtitle_entries(narration, work_dir, video_duration):
472
+ """Narration subtitle entries plus original-dialogue entries in the gaps, sorted by start.
473
+ Original entries are confined to narration gaps, so they never overlap narration entries."""
474
+ entries = list(_subtitle_entries(narration))
475
+ entries.extend(_original_gap_subtitle_entries(narration, work_dir, video_duration))
476
+ entries.sort(key=lambda x: (x["start"], x["end"]))
477
+ return entries
478
+
479
+
480
+ def _source_subtitle_mask_covers_gaps(work_dir=None):
481
+ """Whether the effective source mask hides hardcoded subtitles outside narration."""
482
+ if not _source_subtitle_mask_policy(work_dir).get("active"):
483
+ 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"