@oh-story/dsh 0.1.4 → 0.1.5

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 +2692 -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,376 @@
1
+ """Subtitle text shaping, timing, and measured-canvas geometry."""
2
+
3
+ import os
4
+ import re
5
+
6
+ from lib import CONFIG
7
+ from assemble_constants import (
8
+ SUBTITLE_STYLE_REF_H,
9
+ SUBTITLE_STYLE_REF_W,
10
+ _SUBTITLE_CLOSING_QUOTES,
11
+ _SUBTITLE_TERMINAL_PUNCTUATION,
12
+ )
13
+ from media import _ratio_to_float
14
+
15
+ def _seconds_to_srt_time(seconds):
16
+ """将秒数转为 SRT 时间格式 HH:MM:SS,mmm"""
17
+ h = int(seconds // 3600)
18
+ m = int((seconds % 3600) // 60)
19
+ s = int(seconds % 60)
20
+ ms = int((seconds % 1) * 1000)
21
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
22
+
23
+
24
+ def _seconds_to_ass_time(seconds):
25
+ """将秒数转为 ASS 时间格式 H:MM:SS.cc"""
26
+ centiseconds = int(round(float(seconds) * 100))
27
+ h = centiseconds // 360000
28
+ centiseconds %= 360000
29
+ m = centiseconds // 6000
30
+ centiseconds %= 6000
31
+ s = centiseconds // 100
32
+ cs = centiseconds % 100
33
+ return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
34
+
35
+
36
+ def _subtitle_style_config(canvas=None):
37
+ """Return the internal default burn-in subtitle style.
38
+
39
+ When ``canvas`` ({"width","height"}) is given AND the user has not pinned PlayRes via
40
+ SUBTITLE_PLAY_RES_X/Y, the style is scaled to that canvas: PlayRes is set to the frame
41
+ dimensions (so libass never stretches glyphs — the old hardcoded 1280x720 squished
42
+ portrait text), horizontal metrics scale with width, vertical metrics with height, and
43
+ the font is additionally capped so a full ``max_chars`` line fits the usable width. A
44
+ 16:9 source (or the 1280x720 default) reproduces the legacy values exactly.
45
+ """
46
+ style = {
47
+ "font_name": CONFIG.get("subtitle_font_name", "Arial"),
48
+ "font_size": CONFIG.get("subtitle_font_size", 42),
49
+ "primary_color": CONFIG.get("subtitle_primary_color", "&H00FFFFFF"),
50
+ "outline_color": CONFIG.get("subtitle_outline_color", "&H00000000"),
51
+ "outline": CONFIG.get("subtitle_outline", 2),
52
+ "shadow": CONFIG.get("subtitle_shadow", 1),
53
+ "alignment": CONFIG.get("subtitle_alignment", 2),
54
+ "margin_l": CONFIG.get("subtitle_margin_l", 40),
55
+ "margin_r": CONFIG.get("subtitle_margin_r", 40),
56
+ "margin_v": CONFIG.get("subtitle_margin_v", 30),
57
+ "max_chars": CONFIG.get("subtitle_max_chars", 20),
58
+ "play_res_x": CONFIG.get("subtitle_play_res_x", 1280),
59
+ "play_res_y": CONFIG.get("subtitle_play_res_y", 720),
60
+ }
61
+ pinned = "SUBTITLE_PLAY_RES_X" in os.environ or "SUBTITLE_PLAY_RES_Y" in os.environ
62
+ cw = int((canvas or {}).get("width", 0) or 0)
63
+ ch = int((canvas or {}).get("height", 0) or 0)
64
+ if canvas is None or pinned or cw <= 0 or ch <= 0:
65
+ return style # legacy / manually-pinned: unchanged
66
+
67
+ base_font = float(style["font_size"])
68
+ kx = cw / float(SUBTITLE_STYLE_REF_W) # horizontal metrics ∝ width
69
+ ky = ch / float(SUBTITLE_STYLE_REF_H) # vertical metrics ∝ height
70
+ margin_l = round(float(style["margin_l"]) * kx)
71
+ margin_r = round(float(style["margin_r"]) * kx)
72
+ margin_v = round(float(style["margin_v"]) * ky)
73
+ # height-proportional size, then cap so a full line of CJK glyphs (≈1em wide) fits the
74
+ # usable width — this is what keeps portrait text on-screen instead of overflowing.
75
+ usable_w = max(1.0, cw - margin_l - margin_r)
76
+ width_cap = usable_w / max(1, int(style["max_chars"]))
77
+ font_size = max(1, int(min(base_font * ky, width_cap))) # floor so a full line never overflows
78
+ font_scale = font_size / base_font if base_font else 1.0
79
+ style.update({
80
+ "font_size": font_size,
81
+ "outline": max(0, round(float(style["outline"]) * font_scale)),
82
+ "shadow": max(0, round(float(style["shadow"]) * font_scale)),
83
+ "margin_l": margin_l,
84
+ "margin_r": margin_r,
85
+ "margin_v": margin_v,
86
+ "play_res_x": cw,
87
+ "play_res_y": ch,
88
+ })
89
+ return style
90
+
91
+
92
+ def _validate_measured_subtitle_coordinate_domain(canvas=None):
93
+ y_top = int(CONFIG.get("subtitle_y_top", -1))
94
+ y_bot = int(CONFIG.get("subtitle_y_bot", -1))
95
+ if y_top < 0 and y_bot < 0:
96
+ return
97
+ sar_text = str((canvas or {}).get("sample_aspect_ratio") or "1:1")
98
+ if abs(_ratio_to_float(sar_text, 0.0) - 1.0) >= 1e-9:
99
+ raise ValueError(
100
+ f"字幕带坐标仅支持方形像素画布 (SAR 1:1);当前 SAR={sar_text}"
101
+ )
102
+
103
+
104
+ def _style_for_measured_subtitle_band(style, canvas=None):
105
+ """Fit the ASS baseline and font into explicit auto-rotated display-frame Y coordinates."""
106
+ style = dict(style)
107
+ y_top = int(CONFIG.get("subtitle_y_top", -1))
108
+ y_bot = int(CONFIG.get("subtitle_y_bot", -1))
109
+ canvas_h = int((canvas or {}).get("height", 0) or 0)
110
+ if y_top < 0 and y_bot < 0:
111
+ return style
112
+ _validate_measured_subtitle_coordinate_domain(canvas)
113
+ if canvas_h <= 0:
114
+ return style
115
+ if not 0 <= y_top < y_bot <= canvas_h:
116
+ raise ValueError(
117
+ f"字幕带坐标无效: top={y_top}, bot={y_bot}, 画布高度={canvas_h};"
118
+ "必须满足 0 <= top < bot <= height"
119
+ )
120
+ alignment = int(style.get("alignment", 2))
121
+ if alignment not in {1, 2, 3}:
122
+ raise ValueError(
123
+ "measured subtitle coordinates require a bottom-aligned ASS style "
124
+ f"(SUBTITLE_ALIGNMENT 1/2/3); got {alignment}"
125
+ )
126
+ scale_y = float(style["play_res_y"]) / canvas_h
127
+ style["margin_v"] = max(0, round((canvas_h - y_bot) * scale_y))
128
+ current_font = max(1, int(style["font_size"]))
129
+ current_outline = float(style.get("outline", 0) or 0)
130
+ current_shadow = float(style.get("shadow", 0) or 0)
131
+ safe_area = _measured_subtitle_safe_area(style, canvas)
132
+ available_height = int((safe_area or {}).get("height", 0) or 0)
133
+ fitted_font = current_font
134
+ for candidate in range(current_font, 7, -1):
135
+ scale = candidate / current_font
136
+ outline = max(1 if current_outline > 0 else 0, round(current_outline * scale))
137
+ shadow = max(0, round(current_shadow * scale))
138
+ if candidate * 1.25 + outline * 2 + shadow <= available_height + 1e-6:
139
+ fitted_font = candidate
140
+ break
141
+ else:
142
+ # Keep the renderer's minimum readable size; visual QC will block because it cannot fit.
143
+ fitted_font = min(current_font, 8)
144
+ if fitted_font < current_font:
145
+ scale = fitted_font / current_font
146
+ style["font_size"] = fitted_font
147
+ style["outline"] = max(
148
+ 1 if current_outline > 0 else 0, round(current_outline * scale)
149
+ )
150
+ style["shadow"] = max(0, round(current_shadow * scale))
151
+ return style
152
+
153
+
154
+ def _measured_subtitle_safe_area(style, canvas=None):
155
+ """Return the padded measured band in ASS PlayRes coordinates, or None."""
156
+ y_top = int(CONFIG.get("subtitle_y_top", -1))
157
+ y_bot = int(CONFIG.get("subtitle_y_bot", -1))
158
+ canvas_w = int((canvas or {}).get("width", 0) or 0)
159
+ canvas_h = int((canvas or {}).get("height", 0) or 0)
160
+ if y_top < 0 and y_bot < 0:
161
+ return None
162
+ _validate_measured_subtitle_coordinate_domain(canvas)
163
+ if canvas_w <= 0 or canvas_h <= 0:
164
+ return None
165
+ if not 0 <= y_top < y_bot <= canvas_h:
166
+ raise ValueError(
167
+ f"字幕带坐标无效: top={y_top}, bot={y_bot}, 画布高度={canvas_h};"
168
+ "必须满足 0 <= top < bot <= height"
169
+ )
170
+ padding = max(0, int(CONFIG.get("subtitle_mask_padding", 4) or 0))
171
+ safe_top = max(0, y_top - padding)
172
+ # The ASS style remains bottom-anchored at the measured y_bot. Bottom mask padding hides
173
+ # source glyph edges but is not usable subtitle layout space; only top padding can extend
174
+ # the line box without moving its baseline below the measured band.
175
+ safe_bot = y_bot
176
+ play_x = int(style.get("play_res_x") or canvas_w)
177
+ play_y = int(style.get("play_res_y") or canvas_h)
178
+ scale_y = play_y / canvas_h
179
+ margin_l = int(style.get("margin_l") or 0)
180
+ margin_r = int(style.get("margin_r") or 0)
181
+ return {
182
+ "x": margin_l,
183
+ "y": round(safe_top * scale_y),
184
+ "width": max(1, play_x - margin_l - margin_r),
185
+ "height": max(1, round((safe_bot - safe_top) * scale_y)),
186
+ "bottom_margin": max(0, round((canvas_h - safe_bot) * scale_y)),
187
+ }
188
+
189
+
190
+ def _subtitle_display_text(text):
191
+ """Return display-only subtitle text with trailing sentence punctuation removed.
192
+
193
+ Narration/TTS source text stays untouched; this is applied only to SRT/ASS cue text.
194
+ Closing quotes/brackets are preserved, so 「原声台词。」 renders as 「原声台词」.
195
+ """
196
+ text = str(text or "").strip()
197
+ if not text:
198
+ return ""
199
+ suffix = ""
200
+ while text and text[-1] in _SUBTITLE_CLOSING_QUOTES:
201
+ suffix = text[-1] + suffix
202
+ text = text[:-1].rstrip()
203
+ text = text.rstrip(_SUBTITLE_TERMINAL_PUNCTUATION).rstrip()
204
+ return (text + suffix).strip()
205
+
206
+
207
+ def _subtitle_chunk_weight(text):
208
+ """Weight raw subtitle chunks for timing, independent of display punctuation cleanup."""
209
+ core = re.sub(r"\s+", "", str(text or ""))
210
+ return max(1, len(core))
211
+
212
+
213
+ def _subtitle_entry_chunks(raw_chunks):
214
+ """Pair raw chunks used for timing with their final display text.
215
+
216
+ Timing remains based on the raw split topology. Terminal punctuation is stripped only
217
+ on the emitted text, while quote-only suffix chunks are folded into the previous cue
218
+ so a closing bracket never renders alone.
219
+ """
220
+ chunks = [str(c).strip() for c in (raw_chunks or []) if str(c).strip()]
221
+ out = []
222
+ for i, chunk in enumerate(chunks):
223
+ display = _subtitle_display_text(chunk)
224
+ if not display:
225
+ continue
226
+ if all(ch in _SUBTITLE_CLOSING_QUOTES for ch in display):
227
+ if out:
228
+ out[-1]["text"] += display
229
+ continue
230
+ out.append({"raw": chunk, "text": display})
231
+ return out
232
+
233
+
234
+ def _normalize_subtitle_text(s):
235
+ """Normalize Chinese em-dashes in burned subtitle text: a run of one-or-more "—" (incl. "——")
236
+ collapses to a single ",". Then collapse any resulting double commas (",,"→",") so the dash
237
+ swap never leaves a doubled comma. Empty/None passes through as "" unchanged."""
238
+ text = str(s or "")
239
+ if not text:
240
+ return text
241
+ text = re.sub(r"—+", ",", text)
242
+ text = re.sub(r",{2,}", ",", text)
243
+ return text
244
+
245
+
246
+ def _split_subtitle_chunks(text, max_chars):
247
+ """Split one narration block (often several sentences) into short display chunks.
248
+
249
+ A block is synthesized as one continuous TTS utterance for fluent prosody, but showing the
250
+ whole paragraph as a single subtitle would force a tall multi-line band and lag the picture.
251
+ So we cut the block at punctuation into clauses, then greedily pack adjacent clauses into
252
+ chunks of at most `max_chars` — each chunk renders as ONE readable line synced to its slice of
253
+ the block's audio. Punctuation stays attached here for lossless splitting; the display layer
254
+ strips terminal sentence marks per subtitle-cue style."""
255
+ text = str(text).strip()
256
+ if not text:
257
+ return []
258
+ breakers = ",。!?、;:…—,.!?;:"
259
+ clauses, buf = [], ""
260
+ for ch in text:
261
+ buf += ch
262
+ if ch in breakers:
263
+ clauses.append(buf)
264
+ buf = ""
265
+ if buf.strip():
266
+ clauses.append(buf)
267
+ # Any single clause longer than max_chars is hard-wrapped so no chunk ever exceeds one line.
268
+ # Balance those pieces instead of slicing exactly at max_chars: a 21-character clause must
269
+ # not become a readable 20-character cue followed by a 1-character flash.
270
+ sized = []
271
+ for clause in clauses:
272
+ if len(clause) <= max_chars:
273
+ sized.append(clause)
274
+ else:
275
+ piece_count = (len(clause) + max_chars - 1) // max_chars
276
+ base, extra = divmod(len(clause), piece_count)
277
+ cursor = 0
278
+ for piece_index in range(piece_count):
279
+ width = base + (1 if piece_index < extra else 0)
280
+ sized.append(clause[cursor:cursor + width])
281
+ cursor += width
282
+ chunks, cur = [], ""
283
+ for clause in sized:
284
+ sentence_closed = cur.rstrip().endswith(tuple(_SUBTITLE_TERMINAL_PUNCTUATION))
285
+ if cur and (sentence_closed or len(cur) + len(clause) > max_chars):
286
+ chunks.append(cur)
287
+ cur = clause
288
+ else:
289
+ cur += clause
290
+ if cur.strip():
291
+ chunks.append(cur)
292
+ return [c.strip() for c in chunks if c.strip()]
293
+
294
+
295
+ def _subtitle_entries(narration):
296
+ """Collect subtitle entries from final TTS segment placement.
297
+
298
+ Each placed segment is split into short one-line chunks and its played window
299
+ [actual_place_start, actual_place_end] is distributed across them in proportion to character
300
+ count — karaoke-style timing that keeps each line on screen only while it is roughly being
301
+ spoken, instead of holding a whole paragraph for the segment's full duration."""
302
+ max_chars = int(CONFIG.get("subtitle_max_chars", 20))
303
+ entries = []
304
+ for seg in narration:
305
+ if not isinstance(seg, dict):
306
+ continue
307
+ text = str(seg.get("spoken_text") or seg.get("narration", "")).strip()
308
+ if not text:
309
+ continue
310
+ try:
311
+ start = float(seg.get("actual_place_start", seg["start"]))
312
+ end = float(seg.get("actual_place_end", seg["end"]))
313
+ except (KeyError, TypeError, ValueError):
314
+ continue
315
+ if end - start < 0.1:
316
+ continue
317
+ chunks = _subtitle_entry_chunks(_split_subtitle_chunks(text, max_chars))
318
+ if not chunks:
319
+ continue
320
+ if len(chunks) == 1:
321
+ entries.append({"start": start, "end": end, "text": chunks[0]["text"]})
322
+ continue
323
+ total_chars = sum(_subtitle_chunk_weight(c["raw"]) for c in chunks) or 1
324
+ span = end - start
325
+ cursor = start
326
+ for i, chunk in enumerate(chunks):
327
+ weight = _subtitle_chunk_weight(chunk["raw"])
328
+ chunk_end = end if i == len(chunks) - 1 else cursor + span * (weight / total_chars)
329
+ if i > 0 and chunk_end - cursor < 0.05:
330
+ # slice too short to show on its own — fold the text into the previous line of THIS
331
+ # block (i>0) and extend its end, so no chunk is ever silently dropped.
332
+ entries[-1]["text"] += chunk["text"]
333
+ entries[-1]["end"] = chunk_end
334
+ else:
335
+ entries.append({"start": cursor, "end": chunk_end, "text": chunk["text"]})
336
+ cursor = chunk_end
337
+ return entries
338
+
339
+
340
+ def _distribute_chunks(chunks, start, end):
341
+ """Distribute [start,end] across raw chunks while emitting display-clean text.
342
+
343
+ Terminal subtitle punctuation is visual-only: it is stripped from final cue text,
344
+ but the raw split chunks remain the timing topology.
345
+ """
346
+ chunks = _subtitle_entry_chunks(chunks)
347
+ if not chunks or end - start < 0.1:
348
+ return []
349
+ if len(chunks) == 1:
350
+ return [{"start": start, "end": end, "text": chunks[0]["text"]}]
351
+ total_chars = sum(_subtitle_chunk_weight(c["raw"]) for c in chunks) or 1
352
+ span = end - start
353
+ out, cursor = [], start
354
+ for i, chunk in enumerate(chunks):
355
+ weight = _subtitle_chunk_weight(chunk["raw"])
356
+ chunk_end = end if i == len(chunks) - 1 else cursor + span * (weight / total_chars)
357
+ if out and chunk_end - cursor < 0.05:
358
+ out[-1]["text"] += chunk["text"]
359
+ out[-1]["end"] = chunk_end
360
+ else:
361
+ out.append({"start": cursor, "end": chunk_end, "text": chunk["text"]})
362
+ cursor = chunk_end
363
+ return out
364
+
365
+
366
+ def _bracketed_original_chunks(text, start, end, max_chars):
367
+ """Split original dialogue into timed chunks wrapped in 「」 for visual distinction."""
368
+ raw = str(text).strip()
369
+ if raw.startswith("「") and raw.endswith("」"):
370
+ raw = raw[1:-1].strip()
371
+ chunks = _split_subtitle_chunks(raw, max_chars)
372
+ if chunks:
373
+ chunks = list(chunks)
374
+ chunks[0] = "「" + chunks[0]
375
+ chunks[-1] = chunks[-1] + "」"
376
+ return _distribute_chunks(chunks, start, end)
@@ -0,0 +1,88 @@
1
+ """SRT/ASS serialization for narration and original-dialogue subtitles."""
2
+
3
+ from source_subtitles import _combined_subtitle_entries
4
+ from subtitle_core import (
5
+ _normalize_subtitle_text,
6
+ _seconds_to_ass_time,
7
+ _seconds_to_srt_time,
8
+ _style_for_measured_subtitle_band,
9
+ _subtitle_entries,
10
+ _subtitle_style_config,
11
+ )
12
+
13
+ def _generate_srt(narration, work_dir, video_duration=None):
14
+ """将解说脚本转为 SRT 字幕文件,使用实际音频放置时间。video_duration 给定时,原声留白处补烧原声字幕。"""
15
+ srt_lines = []
16
+ entries = (_subtitle_entries(narration) if video_duration is None
17
+ else _combined_subtitle_entries(narration, work_dir, video_duration))
18
+ # entries are already split into short one-line chunks, so no wrapping here.
19
+ for idx, entry in enumerate(entries, start=1):
20
+ start_ts = _seconds_to_srt_time(entry["start"])
21
+ end_ts = _seconds_to_srt_time(entry["end"])
22
+ srt_lines.append(str(idx))
23
+ srt_lines.append(f"{start_ts} --> {end_ts}")
24
+ srt_lines.append(_normalize_subtitle_text(entry["text"]))
25
+ srt_lines.append("")
26
+ srt_path = work_dir / "subtitles.srt"
27
+ srt_path.write_text("\n".join(srt_lines), encoding="utf-8")
28
+ return srt_path
29
+
30
+
31
+ def _escape_ass_text(text):
32
+ """Escape user text for an ASS dialogue Text field."""
33
+ return (
34
+ str(text)
35
+ .replace("\\", "\\\\")
36
+ .replace("{", "\\{")
37
+ .replace("}", "\\}")
38
+ .replace("\r\n", "\n")
39
+ .replace("\r", "\n")
40
+ .replace("\n", "\\N")
41
+ )
42
+
43
+
44
+ def _generate_ass(narration, work_dir, video_duration=None, canvas=None):
45
+ """Generate an ASS subtitle file for readable hard-sub rendering. video_duration given ⇒ also
46
+ burn the original dialogue (from ASR) during the original-audio gaps. canvas ({"width","height"})
47
+ scales the style to the real frame so portrait/竖屏 subtitles are not stretched."""
48
+ style = _style_for_measured_subtitle_band(_subtitle_style_config(canvas), canvas)
49
+ ass_lines = [
50
+ "[Script Info]",
51
+ "ScriptType: v4.00+",
52
+ "WrapStyle: 0",
53
+ "ScaledBorderAndShadow: yes",
54
+ f"PlayResX: {int(style['play_res_x'])}",
55
+ f"PlayResY: {int(style['play_res_y'])}",
56
+ "",
57
+ "[V4+ Styles]",
58
+ (
59
+ "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, "
60
+ "OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, "
61
+ "ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, "
62
+ "Alignment, MarginL, MarginR, MarginV, Encoding"
63
+ ),
64
+ (
65
+ "Style: Default,"
66
+ f"{style['font_name']},{style['font_size']},{style['primary_color']},&H000000FF,"
67
+ f"{style['outline_color']},&H64000000,0,0,0,0,100,100,0,0,1,"
68
+ f"{style['outline']},{style['shadow']},{style['alignment']},"
69
+ f"{style['margin_l']},{style['margin_r']},{style['margin_v']},1"
70
+ ),
71
+ "",
72
+ "[Events]",
73
+ "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
74
+ ]
75
+ entries = (_subtitle_entries(narration) if video_duration is None
76
+ else _combined_subtitle_entries(narration, work_dir, video_duration))
77
+ # entries are already split into short one-line chunks, so no wrapping here.
78
+ for entry in entries:
79
+ text = _escape_ass_text(_normalize_subtitle_text(entry["text"]))
80
+ ass_lines.append(
81
+ "Dialogue: 0,"
82
+ f"{_seconds_to_ass_time(entry['start'])},{_seconds_to_ass_time(entry['end'])},"
83
+ f"Default,,0,0,0,,{text}"
84
+ )
85
+
86
+ ass_path = work_dir / "subtitles.ass"
87
+ ass_path.write_text("\n".join(ass_lines) + "\n", encoding="utf-8")
88
+ return ass_path