@oh-story/dsh 0.1.3 → 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 (524) hide show
  1. package/README.md +58 -12
  2. package/lib/client.js +2970 -537
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +218 -178
  5. package/lib/drama/skills/short-drama/SKILL.md +110 -117
  6. package/lib/drama/skills/short-drama/references/audience-reveal.md +14 -67
  7. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +26 -77
  8. package/lib/drama/skills/short-drama/references/creator-documents.md +204 -0
  9. package/lib/drama/skills/short-drama/references/creator-workflow.md +73 -63
  10. package/lib/drama/skills/short-drama/references/knowhow-index.md +5 -0
  11. package/lib/drama/skills/short-drama/references/look-development.md +2 -3
  12. package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +14 -48
  13. package/lib/drama/skills/short-drama/references/production-form-profiles.md +1 -1
  14. package/lib/drama/skills/short-drama/references/reference-roles.md +32 -135
  15. package/lib/drama/skills/short-drama/references/runtime-preflight.md +16 -19
  16. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  17. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  18. package/lib/drama/skills/short-drama-assets/SKILL.md +59 -170
  19. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +18 -25
  20. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +13 -15
  21. package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +11 -14
  22. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  23. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +6 -12
  24. package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +8 -11
  25. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +21 -2
  26. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +12 -105
  27. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +12 -32
  28. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  29. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +45 -155
  30. package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +12 -13
  31. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +12 -13
  32. package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +39 -51
  33. package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +3 -4
  34. package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +15 -14
  35. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +11 -15
  36. package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +1 -1
  37. package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +4 -7
  38. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +23 -23
  39. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +8 -65
  40. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  41. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  42. package/lib/drama/skills/short-drama-produce/SKILL.md +67 -15
  43. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  44. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  45. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  46. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  47. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  48. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  49. package/lib/drama/skills/short-drama-review/SKILL.md +61 -123
  50. package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +10 -15
  51. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +15 -16
  52. package/lib/drama/skills/short-drama-review/references/project-calibration.md +7 -9
  53. package/lib/drama/skills/short-drama-review/references/review-method.md +8 -9
  54. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +9 -13
  55. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +104 -4
  56. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +27 -19
  57. package/lib/drama/skills/short-drama-review/references/stage-contract.md +4 -63
  58. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  59. package/lib/drama/skills/short-drama-storyboard/SKILL.md +116 -238
  60. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  61. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +9 -34
  62. package/lib/drama/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
  63. package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +5 -10
  64. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +12 -7
  65. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  66. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +27 -62
  67. package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +16 -25
  68. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +21 -23
  69. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +8 -10
  70. package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +9 -13
  71. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +16 -71
  72. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  73. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +123 -230
  74. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  75. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  76. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +10 -8
  77. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +24 -6
  78. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +2 -2
  79. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  80. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +35 -27
  81. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -11
  82. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +13 -10
  83. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +34 -63
  84. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  85. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  86. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +17 -73
  87. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  88. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  89. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  90. package/lib/drama/skills/short-drama-write/SKILL.md +74 -268
  91. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +7 -0
  92. package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +17 -43
  93. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +15 -15
  94. package/lib/drama/skills/short-drama-write/references/script-craft.md +208 -21
  95. package/lib/drama/skills/short-drama-write/references/stage-contract.md +11 -70
  96. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  97. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  98. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  99. package/lib/index.js +971 -46
  100. package/lib/index.js.map +4 -4
  101. package/lib/novel-to-game/LICENSE +21 -0
  102. package/lib/novel-to-game/README.md +180 -0
  103. package/lib/novel-to-game/README_ZH.md +178 -0
  104. package/lib/novel-to-game/VERSION +1 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  128. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  129. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  130. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  131. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  132. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  133. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  134. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  135. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  136. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  137. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  138. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  139. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  140. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  141. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  142. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  143. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  144. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  145. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  146. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  147. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  148. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  149. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  150. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  151. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  152. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  153. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  154. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  155. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  156. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  157. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  158. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  159. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  160. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  161. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  162. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  163. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  164. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  165. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  166. package/lib/novel-to-game/manifest.json +508 -0
  167. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  168. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  169. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  170. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  171. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  172. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  173. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  174. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  175. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  176. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  177. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  178. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  179. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  180. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  181. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  182. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  183. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  184. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  185. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  186. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  187. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  188. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  189. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  190. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  191. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  192. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  193. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  194. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  195. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  196. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  197. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  198. package/lib/oh-story/manifest.json +419 -309
  199. package/lib/oh-story/roles/character-designer.md +4 -4
  200. package/lib/oh-story/roles/consistency-checker.md +1 -1
  201. package/lib/oh-story/roles/narrative-writer.md +41 -42
  202. package/lib/oh-story/roles/story-architect.md +19 -23
  203. package/lib/oh-story/roles/story-explorer.md +8 -5
  204. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  205. package/lib/oh-story/skills/story/SKILL.md +16 -9
  206. package/lib/oh-story/skills/story/VERSION +1 -1
  207. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  208. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  209. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  210. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  211. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  212. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  213. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  214. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  215. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  216. package/lib/oh-story/skills/story-import/SKILL.md +21 -8
  217. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  218. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  219. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  220. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  221. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  222. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  223. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  224. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  225. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  226. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  227. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  228. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  229. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  230. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  232. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  236. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  237. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  238. package/lib/oh-story/skills/story-long-write/references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
  239. package/lib/oh-story/skills/story-long-write/references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  243. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  244. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  245. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  246. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +5 -4
  247. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  248. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +17 -11
  249. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  250. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  251. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  252. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  253. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  254. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  255. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  256. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  257. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  258. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  259. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  260. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  261. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  262. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  263. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  264. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  265. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  266. package/lib/oh-story/skills/story-setup/SKILL.md +85 -41
  267. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  268. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  269. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  270. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  271. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  272. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  273. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  274. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  275. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  276. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  277. package/lib/oh-story/skills/story-setup/references/agent-references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
  278. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  279. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-setup/references/agent-references/long-reversal.md} +18 -32
  280. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  281. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  282. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  283. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  284. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  285. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → short-reversal.md} +29 -23
  286. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  287. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  288. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  289. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  290. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  291. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  292. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  293. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  294. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  295. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  296. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  297. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  298. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  299. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  300. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  301. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  302. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  303. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  304. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  305. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  306. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  307. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  308. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  309. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  310. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  311. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  312. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  313. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  314. 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
  315. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  316. 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
  317. 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
  318. 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
  319. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  320. 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
  321. 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
  322. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  323. 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
  324. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  325. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  326. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  327. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  328. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  329. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  330. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  331. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  332. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  333. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  334. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  335. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  336. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  337. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  338. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  339. package/lib/types/client/drama-production-view.d.ts +34 -0
  340. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  341. package/lib/types/client/drama-production.d.ts +92 -0
  342. package/lib/types/client/drama-production.d.ts.map +1 -0
  343. package/lib/types/client/file-activity.d.ts +9 -2
  344. package/lib/types/client/file-activity.d.ts.map +1 -1
  345. package/lib/types/client/index.d.ts +1 -1
  346. package/lib/types/client/index.d.ts.map +1 -1
  347. package/lib/types/client/production-intents.d.ts +9 -0
  348. package/lib/types/client/production-intents.d.ts.map +1 -0
  349. package/lib/types/client/production-prompts.d.ts +9 -0
  350. package/lib/types/client/production-prompts.d.ts.map +1 -0
  351. package/lib/types/client/production-runtime.d.ts +58 -0
  352. package/lib/types/client/production-runtime.d.ts.map +1 -0
  353. package/lib/types/client/video-studio.d.ts +41 -0
  354. package/lib/types/client/video-studio.d.ts.map +1 -0
  355. package/lib/types/client/workbench-ui.d.ts +5 -0
  356. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  357. package/lib/types/game-verification.d.ts +16 -0
  358. package/lib/types/game-verification.d.ts.map +1 -0
  359. package/lib/types/index.d.ts +5 -2
  360. package/lib/types/index.d.ts.map +1 -1
  361. package/lib/types/production-intent.d.ts +21 -0
  362. package/lib/types/production-intent.d.ts.map +1 -0
  363. package/lib/types/production-tool.d.ts +5 -0
  364. package/lib/types/production-tool.d.ts.map +1 -0
  365. package/lib/types/reference-tool.d.ts +6 -0
  366. package/lib/types/reference-tool.d.ts.map +1 -0
  367. package/lib/types/role-provider.d.ts.map +1 -1
  368. package/lib/types/role-tool.d.ts +3 -1
  369. package/lib/types/role-tool.d.ts.map +1 -1
  370. package/lib/types/skill-provider.d.ts +7 -0
  371. package/lib/types/skill-provider.d.ts.map +1 -1
  372. package/lib/types/video-project.d.ts +42 -0
  373. package/lib/types/video-project.d.ts.map +1 -0
  374. package/lib/types/workspace-request-trust.d.ts +6 -0
  375. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  376. package/lib/types/workspace-route.d.ts +20 -0
  377. package/lib/types/workspace-route.d.ts.map +1 -1
  378. package/lib/video-recap/CHANGELOG.md +282 -0
  379. package/lib/video-recap/LICENSE +21 -0
  380. package/lib/video-recap/README.en.md +257 -0
  381. package/lib/video-recap/README.md +255 -0
  382. package/lib/video-recap/manifest.json +684 -0
  383. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  384. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  385. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  386. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  387. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  388. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  389. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  390. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  391. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  392. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  393. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  394. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  395. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  396. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  397. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  398. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  399. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  400. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  401. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  402. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  403. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  404. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  405. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  406. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  407. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  408. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  409. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  410. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  411. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  412. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  413. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  414. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  415. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  416. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  417. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  418. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  419. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  420. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  421. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  422. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  423. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  424. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  425. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  426. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  427. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  428. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  429. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  430. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  431. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  432. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  433. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  434. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  435. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  436. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  437. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  438. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  439. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  440. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  441. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  442. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  443. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  444. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  445. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  446. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  447. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  448. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  449. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  450. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  451. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  452. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  453. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  454. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  455. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  456. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  457. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  458. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  459. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  460. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  461. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  462. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  463. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  464. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  465. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  466. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  467. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  468. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  469. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  470. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  471. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  472. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  473. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  474. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  475. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  476. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  477. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  478. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  479. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  480. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  481. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  482. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  483. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  484. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  485. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  486. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  487. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  488. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  489. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  490. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  491. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  492. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  493. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  494. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  495. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  496. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  497. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  498. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  499. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  500. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  501. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  502. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  503. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  504. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  505. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  506. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  507. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  508. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  509. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  510. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  511. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  512. package/package.json +57 -41
  513. package/lib/drama/skills/short-drama/references/lifecycle-commands.md +0 -130
  514. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  515. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  516. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  517. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  518. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  519. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  520. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  521. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  522. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  523. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  524. /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,305 @@
1
+ """Assembly manifest/QC persistence and delivery contract helpers."""
2
+
3
+ import json
4
+ import math
5
+ import wave
6
+ from pathlib import Path
7
+
8
+ from lib import CONFIG
9
+ from assemble_constants import (
10
+ ASSEMBLY_MANIFEST,
11
+ ASSEMBLY_QC,
12
+ SEGMENT_AUDIO_SCHEMA_VERSION,
13
+ VISUAL_QC,
14
+ )
15
+ from audio_mix import _loudness_mode
16
+ from artifacts import (
17
+ _load_work_json,
18
+ _source_video_identity,
19
+ _timeline_provenance_status,
20
+ )
21
+
22
+ def _assembly_manifest_payload(input_video, tts_segments, work_dir, output_path,
23
+ tts_meta_path=None, final_output=None, *, settings_fingerprint):
24
+ """Slim render record. The orchestrator reads `final_output` to report the result;
25
+ `source_video` stays None unless cut mode explicitly passed --source-video, proving a
26
+ stale ambient SOURCE_VIDEO never leaked into a full-mode timeline / 剪映 export."""
27
+ input_video = Path(input_video)
28
+ output_path = Path(output_path)
29
+ source_video, source_video_fingerprint = _source_video_identity()
30
+ qc_path = Path(work_dir) / ASSEMBLY_QC
31
+ qc = _load_work_json(work_dir, ASSEMBLY_QC) if qc_path.exists() else None
32
+ payload = {
33
+ "schema_version": 2,
34
+ "input_video": str(input_video.resolve()),
35
+ "source_video": source_video,
36
+ "source_video_fingerprint": source_video_fingerprint,
37
+ "tts_meta": str(Path(tts_meta_path).resolve()) if tts_meta_path else None,
38
+ "tts_segments": len(tts_segments or []),
39
+ "assembly_settings": settings_fingerprint(work_dir),
40
+ "output_path": str(output_path.resolve()),
41
+ "segment_audio_schema_version": SEGMENT_AUDIO_SCHEMA_VERSION,
42
+ "qc_path": str(qc_path.resolve()) if qc_path.exists() else None,
43
+ "qc_verdict": qc.get("verdict") if isinstance(qc, dict) else None,
44
+ "qc_blocking_codes": qc.get("blocking_codes", []) if isinstance(qc, dict) else [],
45
+ # The settings fingerprint records the configured/fallback loudness policy; these QC
46
+ # fields record what the just-finished render actually used after the loudnorm probe.
47
+ "qc_loudness_mode": qc.get("loudness_mode") if isinstance(qc, dict) else None,
48
+ "qc_loudnorm_measurement": qc.get("loudnorm_measurement") if isinstance(qc, dict) else None,
49
+ "audio_segments": [
50
+ {
51
+ "index": seg.get("index"),
52
+ "segment_audio_schema_version": seg.get("segment_audio_schema_version", SEGMENT_AUDIO_SCHEMA_VERSION),
53
+ "narration": seg.get("narration", ""),
54
+ "spoken_text": seg.get("spoken_text") or seg.get("narration", ""),
55
+ "truncated": bool(seg.get("truncated", False)),
56
+ "truncate_reason": seg.get("truncate_reason", "none"),
57
+ "fit_status": seg.get("fit_status"),
58
+ "blocking": bool(seg.get("blocking", False)),
59
+ "audio_duration": seg.get("audio_duration"),
60
+ "placed_audio_duration": seg.get("placed_audio_duration"),
61
+ "placed_audio_path": seg.get("placed_audio_path"),
62
+ "actual_place_start": seg.get("actual_place_start"),
63
+ "actual_place_end": seg.get("actual_place_end"),
64
+ "source_duck_end": seg.get("source_duck_end"),
65
+ "source_restore_at": seg.get("source_restore_at"),
66
+ "source_handoff_status": seg.get("source_handoff_status"),
67
+ "source_entry_status": seg.get("source_entry_status"),
68
+ "global_narration_speed": seg.get("global_narration_speed"),
69
+ "segment_tempo_factor": seg.get("segment_tempo_factor"),
70
+ "effective_tempo": seg.get("effective_tempo"),
71
+ "rms_dbfs_before": seg.get("rms_dbfs_before"),
72
+ "rms_dbfs_after": seg.get("rms_dbfs_after"),
73
+ "peak_after": seg.get("peak_after"),
74
+ }
75
+ for seg in (tts_segments or [])
76
+ if isinstance(seg, dict)
77
+ ],
78
+ }
79
+ if final_output is not None:
80
+ payload["final_output"] = str(Path(final_output).resolve())
81
+ provenance = _timeline_provenance_status(work_dir)
82
+ if provenance:
83
+ payload["timeline_provenance"] = provenance
84
+ return payload
85
+
86
+
87
+ def _write_assembly_manifest(work_dir, manifest):
88
+ path = Path(work_dir) / ASSEMBLY_MANIFEST
89
+ path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
90
+ return path
91
+
92
+
93
+ def _visual_qc_rollup(visual_qc):
94
+ if not isinstance(visual_qc, dict):
95
+ return {
96
+ "present": False,
97
+ "verdict": "MISSING",
98
+ "blocking": True,
99
+ "blocking_codes": ["missing_visual_qc"],
100
+ "summary": {},
101
+ }
102
+ subtitles = visual_qc.get("subtitles") or visual_qc.get("subtitle") or {}
103
+ overlays = visual_qc.get("overlays") or visual_qc.get("overlay") or {}
104
+ return {
105
+ "present": True,
106
+ "artifact": visual_qc.get("artifact", VISUAL_QC),
107
+ "verdict": visual_qc.get("verdict"),
108
+ "blocking": bool(visual_qc.get("blocking", False)),
109
+ "blocking_codes": list(visual_qc.get("blocking_codes") or []),
110
+ "summary": visual_qc.get("summary", {}),
111
+ "geometry": visual_qc.get("geometry", {}),
112
+ "subtitles": {
113
+ "entries": subtitles.get("entries", 0),
114
+ "multi_line": subtitles.get("multi_line", False),
115
+ "overflow": subtitles.get("overflow", False),
116
+ "safe_area": subtitles.get("safe_area", {}),
117
+ },
118
+ "subtitle": subtitles,
119
+ "mask": visual_qc.get("mask", {}),
120
+ "overlays": {
121
+ "present": overlays.get("present", bool(overlays.get("items"))),
122
+ "rendered": overlays.get("rendered", len(overlays.get("items", [])) if isinstance(overlays.get("items"), list) else 0),
123
+ "unsupported": overlays.get("unsupported", []),
124
+ "overflow": overlays.get("overflow", []),
125
+ },
126
+ "overlay": overlays,
127
+ }
128
+
129
+
130
+ def _build_assembly_qc(tts_segments, video_duration, *, output_path=None,
131
+ source_has_audio=None, loudness_mode=None, loudnorm_measurement=None,
132
+ visual_qc=None, render_delivery=None, delivery_qc=None):
133
+ """Machine-readable assembly release gate.
134
+
135
+ Visual facts are rolled up from visual_qc.json. Delivery/render facts live here
136
+ (or render/delivery QC in future), never in visual_qc.json.
137
+ """
138
+ hard_max = float(CONFIG.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40)
139
+ segments = [s for s in (tts_segments or []) if isinstance(s, dict)]
140
+ no_safe = [
141
+ int(s.get("index", i))
142
+ for i, s in enumerate(segments)
143
+ if s.get("fit_status") == "no_safe_fit"
144
+ or s.get("truncate_reason") in {"no_safe_boundary", "no_room"}
145
+ or bool(s.get("blocking", False))
146
+ ]
147
+ skipped = [
148
+ int(s.get("index", i))
149
+ for i, s in enumerate(segments)
150
+ if s.get("fit_status") == "skipped"
151
+ ]
152
+ fit_failed = [
153
+ int(s.get("index", i))
154
+ for i, s in enumerate(segments)
155
+ if s.get("fit_status") == "speed_adjust_failed"
156
+ ]
157
+ tempo_exceeded = []
158
+ truncated = []
159
+ handoff_failed = []
160
+ timeline_audio_failed = []
161
+ max_effective = 0.0
162
+ for i, s in enumerate(segments):
163
+ try:
164
+ eff = float(s.get("effective_tempo", 0.0) or 0.0)
165
+ except (TypeError, ValueError):
166
+ eff = 0.0
167
+ max_effective = max(max_effective, eff)
168
+ if eff > hard_max + 1e-6:
169
+ tempo_exceeded.append(int(s.get("index", i)))
170
+ if bool(s.get("truncated", False)) or s.get("truncate_reason") == "tail_trim_tolerance":
171
+ truncated.append(int(s.get("index", i)))
172
+ if bool(s.get("source_handoff_blocking", False)):
173
+ handoff_failed.append(int(s.get("index", i)))
174
+ if s.get("actual_place_start") is not None and float(s.get("placed_audio_duration", 0.0) or 0.0) > 0:
175
+ placed_path = s.get("placed_audio_path")
176
+ valid_placed_file = False
177
+ if placed_path and Path(placed_path).exists():
178
+ try:
179
+ with wave.open(str(placed_path), "rb") as placed_wav:
180
+ placed_duration = placed_wav.getnframes() / placed_wav.getframerate()
181
+ tolerance = 1.0 / placed_wav.getframerate()
182
+ timeline_start = round(float(s.get("actual_place_start")), 4)
183
+ timeline_end = math.ceil(
184
+ float(s.get("actual_place_end")) * 10_000 - 1e-9
185
+ ) / 10_000
186
+ serialized_span = timeline_end - timeline_start
187
+ valid_placed_file = abs(
188
+ placed_duration - float(s.get("placed_audio_duration") or 0.0)
189
+ ) <= tolerance + 1e-9 and serialized_span + 1e-9 >= placed_duration
190
+ except (OSError, wave.Error, ZeroDivisionError, TypeError, ValueError):
191
+ valid_placed_file = False
192
+ if not valid_placed_file:
193
+ timeline_audio_failed.append(int(s.get("index", i)))
194
+
195
+ placed = [
196
+ float(s.get("placed_audio_duration", 0.0) or 0.0)
197
+ for s in segments
198
+ if s.get("placed_audio_duration") is not None
199
+ ]
200
+ blocking_codes = []
201
+ if not segments:
202
+ blocking_codes.append("missing_narration")
203
+ if skipped:
204
+ blocking_codes.append("skipped_segments")
205
+ if fit_failed:
206
+ blocking_codes.append("fit_failed")
207
+ if no_safe:
208
+ blocking_codes.append("no_safe_fit")
209
+ if tempo_exceeded:
210
+ blocking_codes.append("effective_tempo_exceeded")
211
+ if truncated:
212
+ blocking_codes.append("truncated_speech")
213
+ if handoff_failed:
214
+ blocking_codes.append("unsafe_source_handoff")
215
+ if timeline_audio_failed:
216
+ blocking_codes.append("timeline_audio_mismatch")
217
+ if placed and max(placed) <= 0.0 and not no_safe:
218
+ blocking_codes.append("empty_narration")
219
+ visual_rollup = (
220
+ _visual_qc_rollup(visual_qc)
221
+ if visual_qc is not None
222
+ else {"present": False, "verdict": "NOT_RUN", "blocking": False, "blocking_codes": [], "summary": {}}
223
+ )
224
+ if visual_rollup.get("blocking"):
225
+ blocking_codes.append("visual_qc_failed")
226
+ if source_has_audio is False:
227
+ # Not blocking: assemble can synthesize a silent original track.
228
+ source_audio = "synthetic_silence"
229
+ elif source_has_audio is True:
230
+ source_audio = "present"
231
+ else:
232
+ source_audio = "unknown"
233
+
234
+ output = {}
235
+ if output_path is not None:
236
+ output_path = Path(output_path)
237
+ output = {
238
+ "path": str(output_path),
239
+ "exists": output_path.exists(),
240
+ "bytes": output_path.stat().st_size if output_path.exists() else 0,
241
+ }
242
+ if output_path.exists() and output["bytes"] <= 0:
243
+ blocking_codes.append("empty_output")
244
+
245
+ render_delivery = render_delivery or delivery_qc or {}
246
+ qc_payload = {
247
+ "schema_version": 1,
248
+ "artifact": ASSEMBLY_QC,
249
+ "verdict": "FAIL" if blocking_codes else "PASS",
250
+ "blocking": bool(blocking_codes),
251
+ "blocking_codes": blocking_codes,
252
+ "duration": round(float(video_duration or 0.0), 4),
253
+ "source_audio": source_audio,
254
+ "loudness_mode": loudness_mode or _loudness_mode(loudnorm_measurement),
255
+ "loudnorm_measurement": loudnorm_measurement,
256
+ "release_gate": {
257
+ "verdict": "FAIL" if blocking_codes else "PASS",
258
+ "visual_qc": visual_rollup.get("verdict"),
259
+ "delivery_qc": "PASS",
260
+ "audio_qc": "FAIL" if any(
261
+ c in blocking_codes
262
+ for c in (
263
+ "missing_narration", "skipped_segments", "fit_failed", "no_safe_fit",
264
+ "effective_tempo_exceeded", "empty_narration", "truncated_speech",
265
+ "unsafe_source_handoff",
266
+ "timeline_audio_mismatch",
267
+ )
268
+ ) else "PASS",
269
+ },
270
+ "visual_qc": visual_rollup,
271
+ "delivery_qc": {
272
+ "video_encode_passes": render_delivery.get("video_encode_passes"),
273
+ "reencode_reason": render_delivery.get("reencode_reason"),
274
+ "audio_sample_rate": render_delivery.get("audio_sample_rate"),
275
+ "final_compat_notes": render_delivery.get("final_compat_notes", []),
276
+ },
277
+ "summary": {
278
+ "segments": len(segments),
279
+ "placed_segments": sum(1 for x in placed if x > 0.0),
280
+ "skipped_segments": skipped,
281
+ "fit_failed_segments": fit_failed,
282
+ "no_safe_fit_segments": no_safe,
283
+ "tempo_exceeded_segments": tempo_exceeded,
284
+ "max_effective_tempo": round(max_effective, 4),
285
+ "truncated_segments": truncated,
286
+ "unsafe_source_handoff_segments": handoff_failed,
287
+ "timeline_audio_mismatch_segments": timeline_audio_failed,
288
+ },
289
+ "output": output,
290
+ }
291
+ qc_payload["visual_rollup"] = visual_rollup
292
+ qc_payload["delivery_rollup"] = qc_payload["delivery_qc"]
293
+ return qc_payload
294
+
295
+
296
+ def _write_assembly_qc(work_dir, qc):
297
+ path = Path(work_dir) / ASSEMBLY_QC
298
+ path.write_text(json.dumps(qc, ensure_ascii=False, indent=2), encoding="utf-8")
299
+ return path
300
+
301
+
302
+ def _resolve_final_output(base, stem):
303
+ """The recap output is the stable human alias recap_<stem>.mp4, overwritten in place
304
+ on every run so the iterate-on-narration loop always refreshes the same file."""
305
+ return Path(base) / f"recap_{stem}.mp4"
@@ -0,0 +1,94 @@
1
+ """Render-affecting settings fingerprint for cache/resume decisions."""
2
+
3
+ from pathlib import Path
4
+
5
+ from artifacts import _artifact_fingerprint
6
+ from assemble_constants import (
7
+ SUBTITLE_RENDER_VERSION,
8
+ SUBTITLE_TEXT_NORMALIZE_VERSION,
9
+ VISUAL_OVERLAYS,
10
+ )
11
+ from audio_mix import _loudness_mode, final_loudnorm_filter
12
+ from lib import CONFIG
13
+ from source_subtitles import _has_user_subtitles, _source_subtitle_mask_policy
14
+ from subtitle_core import _subtitle_style_config
15
+
16
+ def assembly_settings_fingerprint(work_dir=None):
17
+ """Settings that affect the rendered video, used by pipeline resume cache. When work_dir is
18
+ given, a user_subtitles presence flag is included so dropping in a user-subtitle file rebuilds
19
+ the cached subtitles."""
20
+ burn_subtitles = bool(CONFIG.get("burn_subtitles", False))
21
+ mask_policy = _source_subtitle_mask_policy(work_dir)
22
+ mask_source_subtitles = bool(mask_policy["active"])
23
+ user_subtitles = _has_user_subtitles(work_dir)
24
+ overlay_path = Path(work_dir) / VISUAL_OVERLAYS if work_dir is not None else None
25
+ fingerprint = {
26
+ "version": SUBTITLE_RENDER_VERSION,
27
+ "subtitle_text_normalize": SUBTITLE_TEXT_NORMALIZE_VERSION,
28
+ "user_subtitles": user_subtitles,
29
+ "burn_subtitles": burn_subtitles,
30
+ "force_video_reencode": bool(CONFIG.get("force_video_reencode", False)),
31
+ "encode": {
32
+ "output_crf": CONFIG.get("output_crf", 18),
33
+ "output_preset": CONFIG.get("output_preset", "veryfast"),
34
+ "output_max_height": CONFIG.get("output_max_height", 0),
35
+ },
36
+ "video_filters": {
37
+ "mask_source_subtitles": mask_source_subtitles,
38
+ "source_subtitle_mask_policy": mask_policy["policy"],
39
+ "source_subtitle_mask_policy_declared": mask_policy["declared"],
40
+ "source_subtitle_mask_policy_trigger": mask_policy["trigger"],
41
+ "source_subtitle_mask_ratio": (
42
+ CONFIG.get("source_subtitle_mask_ratio", 0.14) if mask_source_subtitles else None
43
+ ),
44
+ "source_subtitle_mask_timing": (
45
+ CONFIG.get("source_subtitle_mask_timing", "narration") if mask_source_subtitles else None
46
+ ),
47
+ "subtitle_mask_opacity": (
48
+ CONFIG.get("subtitle_mask_opacity", 0.6) if mask_source_subtitles else None
49
+ ),
50
+ "subtitle_mask_padding": (
51
+ CONFIG.get("subtitle_mask_padding", 4) if mask_source_subtitles else None
52
+ ),
53
+ "subtitle_y_top": CONFIG.get("subtitle_y_top", -1),
54
+ "subtitle_y_bot": CONFIG.get("subtitle_y_bot", -1),
55
+ "visual_overlays": {
56
+ "artifact": VISUAL_OVERLAYS,
57
+ "present": bool(overlay_path and overlay_path.exists()),
58
+ "fingerprint": _artifact_fingerprint(overlay_path) if overlay_path and overlay_path.exists() else None,
59
+ },
60
+ },
61
+ "narration_timing": {
62
+ "delay_seconds": CONFIG.get("narration_delay_seconds", 0.0),
63
+ "tail_pad_seconds": CONFIG.get("narration_tail_pad_seconds", 0.1),
64
+ "fade_ms": CONFIG.get("fade_ms", 120),
65
+ "narration_speed": CONFIG.get("narration_speed", 1.0),
66
+ "narration_cumulative_tempo_max": CONFIG.get("narration_cumulative_tempo_max", 1.35),
67
+ "tts_segment_tempo_max": CONFIG.get("tts_segment_tempo_max", 1.20),
68
+ },
69
+ "audio_mix": {
70
+ "ducking_mode": CONFIG.get("ducking_mode", "fixed"),
71
+ "duck_fade_seconds": CONFIG.get("duck_fade_seconds", 0.3),
72
+ "duck_bridge_seconds": CONFIG.get("duck_bridge_seconds", 1.5),
73
+ "ducking_narr_weight": CONFIG.get("ducking_narr_weight", 1.5),
74
+ "ducking_orig_volume": CONFIG.get("ducking_orig_volume", 0.3),
75
+ "idle_orig_volume": CONFIG.get("idle_orig_volume", 1.0),
76
+ "speech_ducking_volume": CONFIG.get("speech_ducking_volume", 0.2),
77
+ "zone_ducking_volume": CONFIG.get("zone_ducking_volume", 0.12),
78
+ "ducking_threshold": CONFIG.get("ducking_threshold", 0.15),
79
+ "ducking_ratio": CONFIG.get("ducking_ratio", 3),
80
+ "ducking_attack": CONFIG.get("ducking_attack", 10),
81
+ "ducking_release": CONFIG.get("ducking_release", 300),
82
+ "ducking_level_sc": CONFIG.get("ducking_level_sc", 2.0),
83
+ "ducking_makeup": CONFIG.get("ducking_makeup", 1.2),
84
+ "final_loudnorm": final_loudnorm_filter(),
85
+ "loudness_mode": _loudness_mode(),
86
+ "bgm_path": CONFIG.get("bgm_path", ""),
87
+ "bgm_volume": CONFIG.get("bgm_volume", 0.18),
88
+ "bgm_ducking_volume": CONFIG.get("bgm_ducking_volume", 0.10),
89
+ },
90
+ }
91
+ if burn_subtitles:
92
+ fingerprint["subtitle_renderer"] = "ass"
93
+ fingerprint["subtitle_style"] = _subtitle_style_config()
94
+ return fingerprint
@@ -0,0 +1,205 @@
1
+ """Shared audio automation semantics for ffmpeg render and editable timelines.
2
+
3
+ This module is stdlib-only and is the single source for ducking window coalescing,
4
+ pre-roll/hold/post-roll gain shape, timeline keyframes, and ffmpeg volume terms.
5
+ """
6
+
7
+
8
+ def _round_keyframe(t_s, gain):
9
+ return {"t": round(float(t_s), 4), "gain": round(float(gain), 4)}
10
+
11
+
12
+ def default_bridge(fade):
13
+ return 2 * float(fade or 0)
14
+
15
+
16
+ def coalesce_duck_windows(windows, bridge):
17
+ """Merge [(start, end, gain)] windows separated by gaps below `bridge`.
18
+
19
+ A bridged mixed-level span uses the lowest gain across all members so neither
20
+ ffmpeg nor timeline export swells louder in the bridged gap.
21
+ """
22
+ rel = sorted(
23
+ ([float(s), float(e), float(g)] for s, e, g in windows if float(e) > float(s)),
24
+ key=lambda w: w[0],
25
+ )
26
+ if not rel:
27
+ return []
28
+ bridge = float(bridge or 0)
29
+ merged = [rel[0][:]]
30
+ for s, e, gain in rel[1:]:
31
+ if s - merged[-1][1] < bridge:
32
+ merged[-1][1] = max(merged[-1][1], e)
33
+ merged[-1][2] = min(merged[-1][2], gain)
34
+ else:
35
+ merged.append([s, e, gain])
36
+ return merged
37
+
38
+
39
+ def _t_minus(value):
40
+ value = float(value)
41
+ if value < 0:
42
+ return f"t+{abs(value):.2f}"
43
+ return f"t-{value:.2f}"
44
+
45
+
46
+ def duck_ramp_expression(start, end, fade):
47
+ """Return ffmpeg expression for the canonical duck shape.
48
+
49
+ Semantics: ramp down during [start-fade, start], hold fully ducked on
50
+ [start, end], and release during [end, end+fade].
51
+ """
52
+ start = float(start)
53
+ end = float(end)
54
+ fade = float(fade or 0)
55
+ if fade <= 0:
56
+ return f"between(t,{start:.2f},{end:.2f})"
57
+ ramp_start = start - fade
58
+ ramp_end = end + fade
59
+ return f"min(1,max(0,min({_t_minus(ramp_start)},{ramp_end:.2f}-t)/{fade:.2f}))"
60
+
61
+
62
+ def ducking_expression(windows, idle, fade):
63
+ """Build the ffmpeg volume expression for coalesced duck windows."""
64
+ merged = list(windows or [])
65
+ if not merged:
66
+ return None
67
+ idle = float(idle)
68
+ terms = [
69
+ f"+({float(level) - idle:.3f})*{duck_ramp_expression(s, e, fade)}"
70
+ for s, e, level in merged
71
+ ]
72
+ return f"max(0,min(1,{idle}{''.join(terms)}))"
73
+
74
+
75
+ def coalesce_release_duck_windows(windows, bridge, default_release_fade=0.0):
76
+ """Merge [(start, hold_end, gain, restore_at?)] while preserving safe releases."""
77
+ fade = max(0.0, float(default_release_fade or 0.0))
78
+ rel = []
79
+ for row in windows or []:
80
+ if len(row) < 3:
81
+ continue
82
+ start, end, gain = map(float, row[:3])
83
+ restore = float(row[3]) if len(row) > 3 and row[3] is not None else end + fade
84
+ if end > start:
85
+ rel.append([start, end, gain, max(end, restore)])
86
+ rel.sort(key=lambda row: row[0])
87
+ if not rel:
88
+ return []
89
+ bridge = max(0.0, float(bridge or 0.0))
90
+ merged = [rel[0][:]]
91
+ for start, end, gain, restore in rel[1:]:
92
+ if start - merged[-1][1] < bridge:
93
+ merged[-1][1] = max(merged[-1][1], end)
94
+ merged[-1][2] = min(merged[-1][2], gain)
95
+ merged[-1][3] = max(merged[-1][1], merged[-1][3], restore)
96
+ else:
97
+ merged.append([start, end, gain, restore])
98
+ return merged
99
+
100
+
101
+ def release_ducking_expression(windows, idle, attack_fade, bridge=None):
102
+ """FFmpeg gain expression with a fixed attack and a per-window safe release end."""
103
+ attack = max(0.0, float(attack_fade or 0.0))
104
+ if bridge is None:
105
+ bridge = default_bridge(attack)
106
+ merged = coalesce_release_duck_windows(windows, bridge, attack)
107
+ if not merged:
108
+ return None
109
+ idle = float(idle)
110
+ terms = []
111
+ for start, hold_end, level, restore_at in merged:
112
+ if attack > 0:
113
+ attack_term = f"({_t_minus(start - attack)})/{attack:.4f}"
114
+ else:
115
+ attack_term = f"between(t,{start:.4f},{restore_at:.4f})"
116
+ release = restore_at - hold_end
117
+ if release > 1e-6:
118
+ release_term = f"({restore_at:.4f}-t)/{release:.4f}"
119
+ mask = f"min(1,max(0,min({attack_term},{release_term})))"
120
+ else:
121
+ mask = f"between(t,{start:.4f},{hold_end:.4f})"
122
+ terms.append(f"+({float(level) - idle:.3f})*{mask}")
123
+ return f"max(0,min(1,{idle}{''.join(terms)}))"
124
+
125
+
126
+ def release_ducking_keyframes(windows, idle, attack_fade, span_start, span_end, bridge=None):
127
+ """Timeline keyframes matching `release_ducking_expression` exactly."""
128
+ attack = max(0.0, float(attack_fade or 0.0))
129
+ if bridge is None:
130
+ bridge = default_bridge(attack)
131
+ span_start, span_end = float(span_start), float(span_end)
132
+ normalized = []
133
+ for row in windows or []:
134
+ if len(row) < 3:
135
+ continue
136
+ start, end, gain = map(float, row[:3])
137
+ restore = float(row[3]) if len(row) > 3 and row[3] is not None else end + attack
138
+ if end > span_start and start < span_end and end > start:
139
+ normalized.append((max(span_start, start), min(span_end, end), gain, min(span_end, max(end, restore))))
140
+ merged = coalesce_release_duck_windows(normalized, bridge, attack)
141
+ if not merged:
142
+ return []
143
+ points = [(span_start, float(idle))]
144
+ for start, hold_end, level, restore_at in merged:
145
+ points.extend([
146
+ (max(span_start, start - attack), float(idle)),
147
+ (start, level),
148
+ (hold_end, level),
149
+ (restore_at, float(idle)),
150
+ ])
151
+ points.append((span_end, float(idle)))
152
+ points.sort(key=lambda point: point[0])
153
+ out = []
154
+ for when, gain in points:
155
+ if out and abs(out[-1][0] - when) < 1e-4:
156
+ out[-1] = (when, min(out[-1][1], gain))
157
+ else:
158
+ out.append((when, gain))
159
+ return [_round_keyframe(when, gain) for when, gain in out]
160
+
161
+
162
+ def variable_ducking_keyframes(windows, idle, fade, span_start, span_end, bridge=None):
163
+ """Volume keyframes for per-window duck gains using canonical semantics."""
164
+ fade = float(fade or 0)
165
+ if bridge is None:
166
+ bridge = default_bridge(fade)
167
+ span_start = float(span_start)
168
+ span_end = float(span_end)
169
+ rel = sorted(
170
+ (max(span_start, float(w[0])), min(span_end, float(w[1])), float(w[2]))
171
+ for w in windows
172
+ if float(w[1]) > span_start and float(w[0]) < span_end and float(w[1]) > float(w[0])
173
+ )
174
+ merged = coalesce_duck_windows(rel, bridge)
175
+ if not merged:
176
+ return []
177
+
178
+ pts = [(span_start, float(idle))]
179
+ for s, e, level in merged:
180
+ pts.append((max(span_start, s - fade), float(idle)))
181
+ pts.append((s, level))
182
+ pts.append((e, level))
183
+ pts.append((min(span_end, e + fade), float(idle)))
184
+ pts.append((span_end, float(idle)))
185
+
186
+ pts.sort(key=lambda p: p[0])
187
+ out = []
188
+ for t, gain in pts:
189
+ if out and abs(out[-1][0] - t) < 1e-4:
190
+ out[-1] = (t, min(out[-1][1], gain))
191
+ else:
192
+ out.append((t, gain))
193
+ return [_round_keyframe(t, gain) for t, gain in out]
194
+
195
+
196
+ def fixed_ducking_keyframes(windows, idle, duck, fade, span_start, span_end, bridge=None):
197
+ """Volume keyframes for fixed-gain duck windows using canonical semantics."""
198
+ return variable_ducking_keyframes(
199
+ [(float(s), float(e), float(duck)) for s, e in windows],
200
+ idle,
201
+ fade,
202
+ span_start,
203
+ span_end,
204
+ bridge=bridge,
205
+ )