@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,745 @@
1
+ """Snap clip boundaries to complete speech and clean shot transitions."""
2
+
3
+ import json
4
+
5
+ import re
6
+
7
+ import subprocess
8
+
9
+ from pathlib import Path
10
+
11
+ from lib import log
12
+
13
+
14
+ def _valid_silence_windows(silence_periods):
15
+ """Return sorted well-formed quiet windows as {start,end} floats."""
16
+ windows = []
17
+ for w in silence_periods or []:
18
+ if not isinstance(w, dict):
19
+ continue
20
+ try:
21
+ start = float(w.get("start"))
22
+ end = float(w.get("end"))
23
+ except (TypeError, ValueError):
24
+ continue
25
+ if end >= start:
26
+ windows.append({"start": start, "end": end})
27
+ return sorted(windows, key=lambda w: (w["start"], w["end"]))
28
+
29
+
30
+ def _source_artifact_candidates(
31
+ work_dir, filename, source_id=None, source_work_dir=None
32
+ ):
33
+ candidates = []
34
+ if source_work_dir:
35
+ candidates.append(Path(work_dir) / source_work_dir / filename)
36
+ if source_id not in (None, ""):
37
+ candidates.append(Path(work_dir) / "sources" / str(source_id) / filename)
38
+ candidates.append(Path(work_dir) / filename)
39
+ # Preserve precedence while avoiding duplicate reads when layouts resolve to one path.
40
+ return list(dict.fromkeys(candidates))
41
+
42
+
43
+ def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=None):
44
+ """Load reliable sentence-end pause windows produced by video-understanding.
45
+
46
+ A sentence anchor's `time` is the acoustic pause end, while `pause_start` is already
47
+ after the final spoken sample. Any cut within that closed interval preserves the sentence.
48
+ Low-confidence anchors are deliberately excluded from the hard-safety path.
49
+ """
50
+ for path in _source_artifact_candidates(
51
+ work_dir, "speech_boundary_anchors.json", source_id, source_work_dir
52
+ ):
53
+ if not path.exists():
54
+ continue
55
+ try:
56
+ payload = json.loads(path.read_text(encoding="utf-8"))
57
+ except (OSError, ValueError):
58
+ return []
59
+ anchors = payload.get("sentence_anchors") if isinstance(payload, dict) else None
60
+ windows = []
61
+ for anchor in anchors or []:
62
+ if not isinstance(anchor, dict):
63
+ continue
64
+ confidence = str(anchor.get("confidence") or "").strip().lower()
65
+ if confidence not in {"high", "medium"}:
66
+ continue
67
+ try:
68
+ end = float(anchor.get("time"))
69
+ start = float(anchor.get("pause_start", end))
70
+ except (TypeError, ValueError):
71
+ continue
72
+ start = min(start, end)
73
+ windows.append(
74
+ {
75
+ "start": round(max(0.0, start), 3),
76
+ "end": round(max(0.0, end), 3),
77
+ "kind": "sentence_anchor",
78
+ "confidence": confidence,
79
+ }
80
+ )
81
+ return sorted(windows, key=lambda row: (row["start"], row["end"]))
82
+ return []
83
+
84
+
85
+ def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
86
+ """Best-effort ASR speech ownership spans used only to decide whether an unsafe edge blocks."""
87
+ payload = None
88
+ for filename in ("asr_clean.json", "asr_result.json"):
89
+ for path in _source_artifact_candidates(
90
+ work_dir, filename, source_id, source_work_dir
91
+ ):
92
+ if not path.exists():
93
+ continue
94
+ try:
95
+ payload = json.loads(path.read_text(encoding="utf-8"))
96
+ except (OSError, ValueError):
97
+ payload = None
98
+ if payload is not None:
99
+ break
100
+ if payload is not None:
101
+ break
102
+ if isinstance(payload, dict):
103
+ payload = payload.get("segments") or payload.get("items") or []
104
+ spans = []
105
+ for row in payload or []:
106
+ if not isinstance(row, dict) or not str(row.get("text") or "").strip():
107
+ continue
108
+ try:
109
+ start, end = float(row.get("start")), float(row.get("end"))
110
+ except (TypeError, ValueError):
111
+ continue
112
+ if end > start:
113
+ spans.append({"start": max(0.0, start), "end": end})
114
+ merged = []
115
+ for span in sorted(spans, key=lambda row: (row["start"], row["end"])):
116
+ if merged and span["start"] <= merged[-1]["end"] + 0.05:
117
+ merged[-1]["end"] = max(merged[-1]["end"], span["end"])
118
+ else:
119
+ merged.append(dict(span))
120
+ return merged
121
+
122
+
123
+ def _combine_boundary_windows(*groups):
124
+ rows = []
125
+ for group in groups:
126
+ rows.extend(_valid_silence_windows(group))
127
+ unique = {(round(row["start"], 3), round(row["end"], 3)) for row in rows}
128
+ return [{"start": start, "end": end} for start, end in sorted(unique)]
129
+
130
+
131
+ def _same_source(left, right):
132
+ left_id, right_id = left.get("source_id"), right.get("source_id")
133
+ if left_id not in (None, "") or right_id not in (None, ""):
134
+ return (
135
+ left_id not in (None, "")
136
+ and right_id not in (None, "")
137
+ and str(left_id) == str(right_id)
138
+ )
139
+ left_path, right_path = left.get("source_path"), right.get("source_path")
140
+ if left_path or right_path:
141
+ return bool(left_path and right_path and str(left_path) == str(right_path))
142
+ return True # single-source normalized plans intentionally omit source identity
143
+
144
+
145
+ def _continuous_source_join(left, right, tolerance=0.05):
146
+ return bool(
147
+ _same_source(left, right)
148
+ and abs(
149
+ float(left.get("source_end", 0.0)) - float(right.get("source_start", 0.0))
150
+ )
151
+ <= tolerance
152
+ and abs(
153
+ float(left.get("output_end", 0.0)) - float(right.get("output_start", 0.0))
154
+ )
155
+ <= tolerance
156
+ )
157
+
158
+
159
+ def enforce_clip_sentence_boundaries(
160
+ plan, boundary_windows, speech_spans, video_duration, tolerance=0.05
161
+ ):
162
+ """Block any audible clip edge that falls inside detected source speech.
163
+
164
+ Safe edges are: source start/end, a reliable sentence/quiet pause, or a truly contiguous
165
+ same-source join (no media is removed). Missing ASR timing degrades to `unchecked` rather
166
+ than inventing speech. Once ASR says an edge is speech-owned, failure to snap is blocking.
167
+ """
168
+ clips = plan.get("clips") or []
169
+ windows = _valid_silence_windows(boundary_windows)
170
+ spans = _valid_silence_windows(speech_spans)
171
+ video_duration = max(0.0, float(video_duration or 0.0))
172
+ tolerance = max(0.001, float(tolerance or 0.05))
173
+ checks, new_blockers = [], []
174
+
175
+ def in_windows(ts):
176
+ return any(
177
+ row["start"] - tolerance <= ts <= row["end"] + tolerance for row in windows
178
+ )
179
+
180
+ def in_speech(ts):
181
+ return any(
182
+ row["start"] - tolerance <= ts <= row["end"] + tolerance for row in spans
183
+ )
184
+
185
+ for idx, clip in enumerate(clips):
186
+ for edge, ts in (
187
+ ("start", float(clip["source_start"])),
188
+ ("end", float(clip["source_end"])),
189
+ ):
190
+ contiguous = (
191
+ edge == "start"
192
+ and idx > 0
193
+ and _continuous_source_join(clips[idx - 1], clip, tolerance)
194
+ ) or (
195
+ edge == "end"
196
+ and idx + 1 < len(clips)
197
+ and _continuous_source_join(clip, clips[idx + 1], tolerance)
198
+ )
199
+ if edge == "start" and ts <= tolerance:
200
+ status, reason = "safe", "source_start"
201
+ elif edge == "end" and ts >= video_duration - tolerance:
202
+ status, reason = "safe", "source_end"
203
+ elif contiguous:
204
+ status, reason = "safe", "continuous_source_join"
205
+ elif in_windows(ts):
206
+ status, reason = "safe", "sentence_or_quiet_boundary"
207
+ elif not spans:
208
+ status, reason = "unchecked", "speech_timing_unavailable"
209
+ elif not in_speech(ts):
210
+ status, reason = "safe", "outside_detected_speech"
211
+ else:
212
+ status, reason = "blocking", "inside_detected_speech"
213
+ check = {
214
+ "clip_id": clip.get("clip_id", idx),
215
+ "source_id": clip.get("source_id"),
216
+ "edge": edge,
217
+ "time": round(ts, 3),
218
+ "status": status,
219
+ "reason": reason,
220
+ }
221
+ checks.append(check)
222
+ if status == "blocking":
223
+ new_blockers.append(
224
+ {
225
+ "code": "unsafe_clip_sentence_boundary",
226
+ **check,
227
+ "message": "剪辑边界仍落在原声讲话区间内,必须移动到句末锚点,不能截断原声句子。",
228
+ }
229
+ )
230
+
231
+ qc = plan.setdefault("qc", {})
232
+ qc.setdefault("boundary_status", {})["sentence_checks"] = checks
233
+ existing = [
234
+ row
235
+ for row in (qc.get("blocking") or [])
236
+ if not isinstance(row, dict)
237
+ or row.get("code") != "unsafe_clip_sentence_boundary"
238
+ ]
239
+ if existing or new_blockers:
240
+ qc["blocking"] = existing + new_blockers
241
+ else:
242
+ qc.pop("blocking", None)
243
+ return plan
244
+
245
+
246
+ def _recompute_clip_timeline(clips):
247
+ cursor = 0.0
248
+ for clip in clips:
249
+ duration = round(float(clip["source_end"]) - float(clip["source_start"]), 3)
250
+ clip["duration"] = duration
251
+ clip["output_start"] = round(cursor, 3)
252
+ clip["output_end"] = round(cursor + duration, 3)
253
+ cursor += duration
254
+ return round(cursor, 3)
255
+
256
+
257
+ def _candidate_overlaps(clips, idx, new_start, new_end):
258
+ for j, other in enumerate(clips):
259
+ if j == idx:
260
+ continue
261
+ if new_start < float(other["source_end"]) and new_end > float(
262
+ other["source_start"]
263
+ ):
264
+ return True
265
+ return False
266
+
267
+
268
+ def snap_clip_starts_to_lines(
269
+ plan,
270
+ silence_periods,
271
+ video_duration,
272
+ max_prepend,
273
+ max_trim=0.35,
274
+ min_clip_duration=0.3,
275
+ ):
276
+ """Snap clip starts to natural quiet boundaries, preferring safe prepend over trim.
277
+
278
+ Policy:
279
+ - start already inside a quiet window: keep.
280
+ - speech start: prepend to nearest prior quiet window end if within max_prepend.
281
+ - no usable prior quiet: keep and warn, except an extremely near next quiet start
282
+ (<= max_trim) may trim forward if duration/overlap safety holds.
283
+ - never overlap/collapse when allow_overlap is false; unsafe attempts keep-and-warn.
284
+ """
285
+ silence_periods = _valid_silence_windows(silence_periods)
286
+ if not silence_periods:
287
+ return plan
288
+
289
+ clips = [dict(c) for c in plan["clips"]]
290
+ video_duration = max(0.0, float(video_duration or 0.0))
291
+ max_prepend = max(0.0, float(max_prepend or 0.0))
292
+ max_trim = max(0.0, float(max_trim or 0.0))
293
+ min_duration = max(0.05, float(min_clip_duration or 0.05))
294
+ allow_overlap = bool(plan.get("allow_overlap", False))
295
+ events = []
296
+
297
+ for i, clip in enumerate(clips):
298
+ original_start = float(clip["source_start"])
299
+ source_end = float(clip["source_end"])
300
+ event = {
301
+ "clip_id": clip.get("clip_id", i),
302
+ "source_id": clip.get("source_id"),
303
+ "original_start": round(original_start, 3),
304
+ "action": "kept",
305
+ }
306
+
307
+ if any(w["start"] <= original_start <= w["end"] for w in silence_periods):
308
+ event["reason"] = "already_quiet"
309
+ events.append(event)
310
+ continue
311
+
312
+ prior_ends = [w["end"] for w in silence_periods if w["end"] <= original_start]
313
+ if prior_ends:
314
+ candidate_start = max(prior_ends)
315
+ if original_start - candidate_start <= max_prepend:
316
+ candidate_start = round(max(0.0, candidate_start), 3)
317
+ safe = candidate_start < source_end - min_duration + 1e-9
318
+ if safe and not allow_overlap:
319
+ safe = not _candidate_overlaps(
320
+ clips, i, candidate_start, source_end
321
+ )
322
+ if safe:
323
+ clip["source_start"] = candidate_start
324
+ event.update(
325
+ {
326
+ "action": "prepended",
327
+ "new_start": candidate_start,
328
+ "delta": round(original_start - candidate_start, 3),
329
+ }
330
+ )
331
+ events.append(event)
332
+ continue
333
+ reason = "overlap_or_collapse"
334
+ else:
335
+ reason = "prior_quiet_too_far"
336
+ else:
337
+ reason = "no_prior_quiet"
338
+
339
+ next_starts = [
340
+ w["start"] for w in silence_periods if w["start"] >= original_start
341
+ ]
342
+ if next_starts:
343
+ candidate_start = min(next_starts)
344
+ trim_delta = candidate_start - original_start
345
+ if 0 < trim_delta <= max_trim:
346
+ candidate_start = round(min(video_duration, candidate_start), 3)
347
+ safe = source_end - candidate_start >= min_duration
348
+ if safe and not allow_overlap:
349
+ safe = not _candidate_overlaps(
350
+ clips, i, candidate_start, source_end
351
+ )
352
+ if safe:
353
+ clip["source_start"] = candidate_start
354
+ event.update(
355
+ {
356
+ "action": "trimmed",
357
+ "new_start": candidate_start,
358
+ "delta": round(trim_delta, 3),
359
+ "fallback_from": reason,
360
+ }
361
+ )
362
+ events.append(event)
363
+ continue
364
+ reason = "unsafe_forward_trim"
365
+
366
+ event["start_unsnapped_reason"] = reason
367
+ event["warning_code"] = "clip_start_unsnapped"
368
+ events.append(event)
369
+
370
+ total_duration = _recompute_clip_timeline(clips)
371
+ result = dict(plan)
372
+ result["clips"] = clips
373
+ result["total_duration"] = total_duration
374
+ qc = dict(result.get("qc") or {})
375
+ boundary = dict(qc.get("boundary_status") or {})
376
+ boundary["start_snaps"] = events
377
+ qc["boundary_status"] = boundary
378
+ warnings = list(qc.get("warnings") or [])
379
+ for event in events:
380
+ if event.get("warning_code"):
381
+ warnings.append(
382
+ {
383
+ "code": event["warning_code"],
384
+ "clip_id": event["clip_id"],
385
+ "source_id": event.get("source_id"),
386
+ "start_unsnapped_reason": event.get("start_unsnapped_reason"),
387
+ }
388
+ )
389
+ if warnings:
390
+ qc["warnings"] = warnings
391
+ result["qc"] = qc
392
+ return result
393
+
394
+
395
+ def snap_clip_ends_to_lines(plan, silence_periods, video_duration, max_extend):
396
+ """Extend each clip's source_end forward to the next natural pause, preventing mid-sentence cuts.
397
+
398
+ - If silence_periods is empty/None, returns plan unchanged.
399
+ - If a clip's source_end already falls inside a quiet window, no snap is applied.
400
+ - Otherwise, extends to the next quiet window start, capped by max_extend and video_duration.
401
+ - When plan["allow_overlap"] is False, will not extend into another clip's source range.
402
+ - After snapping, recomputes output_start/output_end/duration for all clips cursor-based.
403
+ - Returns the updated plan dict (all other keys preserved).
404
+ """
405
+ # silence_periods is loaded from disk; tolerate a stale/hand-edited row by keeping only
406
+ # well-formed quiet windows so a single bad entry can't KeyError-abort the whole cut.
407
+ silence_periods = _valid_silence_windows(silence_periods)
408
+ if not silence_periods:
409
+ return plan
410
+
411
+ clips = [dict(c) for c in plan["clips"]]
412
+ video_duration = float(video_duration)
413
+ max_extend = float(max_extend)
414
+ allow_overlap = bool(plan.get("allow_overlap", False))
415
+ events = []
416
+
417
+ for i, clip in enumerate(clips):
418
+ source_end = clip["source_end"]
419
+ event = {
420
+ "clip_id": clip.get("clip_id", i),
421
+ "source_id": clip.get("source_id"),
422
+ "original_end": round(float(source_end), 3),
423
+ "action": "kept",
424
+ }
425
+
426
+ # Already inside a quiet window → already at a natural pause.
427
+ if any(w["start"] <= source_end <= w["end"] for w in silence_periods):
428
+ event["reason"] = "already_quiet"
429
+ events.append(event)
430
+ continue
431
+
432
+ # Find the next quiet window start at or after source_end.
433
+ candidates = [w["start"] for w in silence_periods if w["start"] >= source_end]
434
+ if not candidates:
435
+ event["end_unsnapped_reason"] = "no_next_quiet"
436
+ events.append(event)
437
+ continue
438
+ next_quiet_start = min(candidates)
439
+
440
+ # Only snap if the next pause is within reach (≤ max_extend away).
441
+ if next_quiet_start > source_end + max_extend:
442
+ event["end_unsnapped_reason"] = "next_quiet_too_far"
443
+ events.append(event)
444
+ continue
445
+ candidate_end = min(next_quiet_start, video_duration)
446
+ if candidate_end <= source_end:
447
+ event["end_unsnapped_reason"] = "non_forward_candidate"
448
+ events.append(event)
449
+ continue
450
+
451
+ # When overlaps are forbidden, cap against every other clip's source range.
452
+ if not allow_overlap:
453
+ other_starts = [
454
+ c["source_start"]
455
+ for j, c in enumerate(clips)
456
+ if j != i and c["source_start"] > source_end
457
+ ]
458
+ if other_starts:
459
+ nearest_other_start = min(other_starts)
460
+ candidate_end = min(candidate_end, nearest_other_start)
461
+ if candidate_end <= source_end:
462
+ event["end_unsnapped_reason"] = "overlap_or_collapse"
463
+ events.append(event)
464
+ continue
465
+
466
+ clip["source_end"] = round(candidate_end, 3)
467
+ event.update(
468
+ {
469
+ "action": "extended",
470
+ "new_end": clip["source_end"],
471
+ "delta": round(float(clip["source_end"]) - float(source_end), 3),
472
+ }
473
+ )
474
+ events.append(event)
475
+
476
+ # Recompute output timeline cursor-based (same cursor logic as normalize_clip_plan).
477
+ total_duration = _recompute_clip_timeline(clips)
478
+
479
+ result = dict(plan)
480
+ result["clips"] = clips
481
+ result["total_duration"] = total_duration
482
+ qc = dict(result.get("qc") or {})
483
+ boundary = dict(qc.get("boundary_status") or {})
484
+ boundary["end_snaps"] = events
485
+ qc["boundary_status"] = boundary
486
+ result["qc"] = qc
487
+ return result
488
+
489
+
490
+ def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
491
+ """Absolute source-time hard cuts inside [win_start, win_end] via ffmpeg's scene metric.
492
+
493
+ Input-seek to a little before the window: the rebased output PTS restarts at ~0 at the seek
494
+ target, so `seek + pts_time` recovers absolute source time. The `lead` keeps the seek/keyframe
495
+ settling artifact frame outside [win_start, win_end] so it is filtered out, not mistaken for a
496
+ cut. Returns [] on any ffmpeg trouble (advisory pass must never block the cut).
497
+ """
498
+ win_start = max(0.0, float(win_start))
499
+ win_end = max(win_start, float(win_end))
500
+ if win_end - win_start < 1e-3:
501
+ return []
502
+ seek = max(0.0, win_start - lead)
503
+ dur = (win_end - seek) + 0.1
504
+ cmd = [
505
+ "ffmpeg",
506
+ "-hide_banner",
507
+ "-nostats",
508
+ "-ss",
509
+ f"{seek:.3f}",
510
+ "-i",
511
+ str(video),
512
+ "-t",
513
+ f"{dur:.3f}",
514
+ "-an",
515
+ "-sn",
516
+ "-filter:v",
517
+ f"select='gt(scene,{threshold})',showinfo",
518
+ "-f",
519
+ "null",
520
+ "-",
521
+ ]
522
+ try:
523
+ proc = subprocess.run(cmd, capture_output=True, text=True)
524
+ except (OSError, ValueError):
525
+ return []
526
+ changes = []
527
+ for m in re.finditer(r"pts_time:([0-9.]+)", proc.stderr or ""):
528
+ t = seek + float(m.group(1))
529
+ if win_start <= t <= win_end:
530
+ changes.append(round(t, 3))
531
+ return sorted(set(changes))
532
+
533
+
534
+ def snap_clips_off_shot_changes(
535
+ plan, video, video_duration, margin, threshold, min_keep=0.5
536
+ ):
537
+ """Nudge each clip's boundaries clear of the ORIGINAL footage's hard cuts to avoid 闪烁.
538
+
539
+ A clip whose source_start sits just before a shot-change opens on a brief sliver of the old
540
+ shot that then hard-cuts again; one whose source_end sits just after a shot-change closes on a
541
+ sliver of the next shot. Both flash at the edit point. So:
542
+ - move source_start FORWARD onto a shot-change in (start, start+margin] → clean open
543
+ - move source_end BACK onto a shot-change in [end-margin, end) → clean close
544
+ Boundaries already on a cut, or with no nearby cut, are left untouched. Snaps that would shrink
545
+ a clip below `min_keep` are skipped. Recomputes the output timeline cursor-based. Advisory: any
546
+ detection failure leaves that boundary as-is.
547
+ """
548
+ del video_duration # Positional compatibility for downstream direct callers.
549
+ margin = float(margin)
550
+ if margin <= 0 or not plan.get("clips"):
551
+ return plan
552
+ clips = [dict(c) for c in plan["clips"]]
553
+ n_start = n_end = 0
554
+ events = []
555
+ for i, clip in enumerate(clips):
556
+ s = float(clip["source_start"])
557
+ e = float(clip["source_end"])
558
+ new_s, new_e = s, e
559
+ event = {
560
+ "clip_id": clip.get("clip_id", i),
561
+ "source_id": clip.get("source_id"),
562
+ "original_start": round(s, 3),
563
+ "original_end": round(e, 3),
564
+ "start_action": "kept",
565
+ "end_action": "kept",
566
+ }
567
+ # Opening: a shot-change just AFTER source_start leaves an old-shot sliver before it.
568
+ start_changes = [
569
+ c
570
+ for c in _detect_shot_changes(video, s, min(e, s + margin), threshold)
571
+ if c > s + 1e-3
572
+ ]
573
+ if start_changes:
574
+ cand = max(start_changes) # open after the last rapid cut in the window
575
+ if cand < e - min_keep:
576
+ new_s = round(cand, 3)
577
+ event["start_action"] = "moved_forward"
578
+ event["new_start"] = new_s
579
+ else:
580
+ event["start_unsnapped_reason"] = "collapse"
581
+ # Closing: a shot-change just BEFORE source_end leaves a next-shot sliver after it.
582
+ end_changes = [
583
+ c
584
+ for c in _detect_shot_changes(video, max(new_s, e - margin), e, threshold)
585
+ if c < e - 1e-3
586
+ ]
587
+ if end_changes:
588
+ cand = min(end_changes) # close before the first rapid cut in the window
589
+ if cand > new_s + min_keep:
590
+ new_e = round(cand, 3)
591
+ event["end_action"] = "moved_back"
592
+ event["new_end"] = new_e
593
+ else:
594
+ event["end_unsnapped_reason"] = "collapse"
595
+ n_start += new_s != s
596
+ n_end += new_e != e
597
+ clip["source_start"] = new_s
598
+ clip["source_end"] = new_e
599
+ events.append(event)
600
+
601
+ # Recompute output timeline cursor-based (same cursor logic as snap_clip_ends_to_lines).
602
+ total_duration = _recompute_clip_timeline(clips)
603
+
604
+ if n_start or n_end:
605
+ log(
606
+ f"避让原片切镜头: {n_start} 个起点前移、{n_end} 个终点回收 (margin={margin}s, 阈值={threshold})"
607
+ )
608
+ result = dict(plan)
609
+ result["clips"] = clips
610
+ result["total_duration"] = total_duration
611
+ qc = dict(result.get("qc") or {})
612
+ boundary = dict(qc.get("boundary_status") or {})
613
+ boundary["shot_snaps"] = events
614
+ qc["boundary_status"] = boundary
615
+ result["qc"] = qc
616
+ return result
617
+
618
+
619
+ def _load_silence_for_source(work_dir, source_id, source_work_dir=None):
620
+ """Read a source's silence_periods.json from the multi-source work layout (best-effort)."""
621
+ for path in _source_artifact_candidates(
622
+ work_dir, "silence_periods.json", source_id, source_work_dir
623
+ ):
624
+ if path.exists():
625
+ try:
626
+ data = json.loads(path.read_text(encoding="utf-8"))
627
+ except (OSError, ValueError):
628
+ return []
629
+ return data if isinstance(data, list) else []
630
+ return []
631
+
632
+
633
+ def snap_multi_source_clips(
634
+ plan,
635
+ sources,
636
+ work_dir,
637
+ *,
638
+ line_max_extend,
639
+ scene_margin,
640
+ scene_threshold,
641
+ do_line_snap=True,
642
+ do_scene_snap=True,
643
+ start_max_prepend=None,
644
+ start_max_trim=None,
645
+ ):
646
+ """Per-source line/shot snapping for a multi-source validated plan.
647
+
648
+ Each clip is snapped using ITS OWN source's silence windows / shot changes and duration
649
+ (a clip in source B never constrains a clip in source A), then the global OUTPUT timeline
650
+ is recomputed once in plan order. Advisory: missing silence data or ffmpeg trouble leaves a
651
+ boundary unchanged. Mirrors the single-source snap_clip_ends_to_lines + snap_clips_off_shot_changes.
652
+ """
653
+ clips = plan.get("clips") or []
654
+ if not clips:
655
+ return plan
656
+ allow_overlap = bool(plan.get("allow_overlap", False))
657
+ groups = {}
658
+ for clip in clips:
659
+ groups.setdefault(str(clip.get("source_id")), []).append(clip)
660
+ boundary_accum = {
661
+ "start_snaps": [],
662
+ "end_snaps": [],
663
+ "shot_snaps": [],
664
+ "sentence_checks": [],
665
+ }
666
+ blocking_accum = []
667
+ for sid, group in groups.items():
668
+ source = sources.get(sid, {}) if isinstance(sources, dict) else {}
669
+ duration = float(source.get("duration") or 0.0) or max(
670
+ (float(c["source_end"]) for c in group), default=0.0
671
+ )
672
+ mini = {"clips": [dict(c) for c in group], "allow_overlap": allow_overlap}
673
+ # Visual cleanup goes first. Sentence/quiet snapping is the final authority because
674
+ # a clean picture is never allowed to reintroduce a mid-sentence audio cut.
675
+ if do_scene_snap and source.get("source_path"):
676
+ mini = snap_clips_off_shot_changes(
677
+ mini,
678
+ source["source_path"],
679
+ video_duration=duration,
680
+ margin=scene_margin,
681
+ threshold=scene_threshold,
682
+ )
683
+ source_work_dir = source.get("source_work_dir")
684
+ silence = _load_silence_for_source(work_dir, sid, source_work_dir)
685
+ anchors = _load_sentence_boundary_windows(work_dir, sid, source_work_dir)
686
+ boundaries = _combine_boundary_windows(silence, anchors)
687
+ if do_line_snap:
688
+ if start_max_prepend is not None:
689
+ mini = snap_clip_starts_to_lines(
690
+ mini,
691
+ boundaries,
692
+ duration,
693
+ start_max_prepend,
694
+ max_trim=start_max_trim if start_max_trim is not None else 0.35,
695
+ )
696
+ mini = snap_clip_ends_to_lines(mini, boundaries, duration, line_max_extend)
697
+ mini = enforce_clip_sentence_boundaries(
698
+ mini,
699
+ boundaries,
700
+ _load_source_speech_spans(work_dir, sid, source_work_dir),
701
+ duration,
702
+ )
703
+ mini_boundary = (mini.get("qc") or {}).get("boundary_status") or {}
704
+ for key in boundary_accum:
705
+ boundary_accum[key].extend(
706
+ [e for e in mini_boundary.get(key, []) if isinstance(e, dict)]
707
+ )
708
+ blocking_accum.extend(
709
+ [
710
+ row
711
+ for row in ((mini.get("qc") or {}).get("blocking") or [])
712
+ if isinstance(row, dict)
713
+ ]
714
+ )
715
+ for original, snapped in zip(group, mini["clips"]):
716
+ original["source_start"] = snapped["source_start"]
717
+ original["source_end"] = snapped["source_end"]
718
+ # Recompute the global output timeline cursor-based, in plan order (not group order).
719
+ plan["total_duration"] = _recompute_clip_timeline(clips)
720
+
721
+ # Preserve per-source boundary QC as the single validated-plan QC source.
722
+ if any(boundary_accum.values()):
723
+ qc = plan.setdefault("qc", {})
724
+ boundary = qc.setdefault("boundary_status", {})
725
+ for key, events in boundary_accum.items():
726
+ if events:
727
+ boundary.setdefault(key, []).extend(events)
728
+ warnings = qc.setdefault("warnings", [])
729
+ for event in boundary_accum["start_snaps"]:
730
+ if event.get("warning_code"):
731
+ warnings.append(
732
+ {
733
+ "code": event["warning_code"],
734
+ "clip_id": event.get("clip_id"),
735
+ "source_id": event.get("source_id"),
736
+ "start_unsnapped_reason": event.get("start_unsnapped_reason"),
737
+ }
738
+ )
739
+ # Blocking rows are propagated unconditionally. They used to sit inside the
740
+ # `any(boundary_accum.values())` branch, which happens to hold today only because
741
+ # enforce_clip_sentence_boundaries always emits sentence_checks — a mid-sentence cut
742
+ # must never be silently dropped because some sibling QC list came back empty.
743
+ if blocking_accum:
744
+ plan.setdefault("qc", {}).setdefault("blocking", []).extend(blocking_accum)
745
+ return plan