@oh-story/dsh 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (479) hide show
  1. package/README.md +56 -12
  2. package/lib/client.js +2692 -268
  3. package/lib/client.js.map +4 -4
  4. package/lib/drama/manifest.json +136 -101
  5. package/lib/drama/skills/short-drama/SKILL.md +61 -1
  6. package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
  8. package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
  9. package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
  10. package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
  11. package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
  12. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
  13. package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
  14. package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
  15. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
  16. package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
  17. package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
  18. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  19. package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
  20. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
  21. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
  22. package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
  23. package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
  24. package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
  25. package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
  26. package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
  27. package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
  28. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
  29. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
  30. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
  31. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
  32. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
  33. package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
  34. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
  35. package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
  36. package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
  37. package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
  38. package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
  39. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
  40. package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
  41. package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
  42. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
  43. package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
  44. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
  45. package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
  46. package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
  47. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
  50. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
  51. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
  53. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
  54. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
  55. package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
  56. package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
  57. package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
  58. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
  59. package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
  60. package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
  61. package/lib/index.js +865 -33
  62. package/lib/index.js.map +4 -4
  63. package/lib/novel-to-game/LICENSE +21 -0
  64. package/lib/novel-to-game/README.md +180 -0
  65. package/lib/novel-to-game/README_ZH.md +178 -0
  66. package/lib/novel-to-game/VERSION +1 -0
  67. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
  68. package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  69. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
  70. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  71. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  72. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  73. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  74. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  75. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  76. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  77. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  78. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  79. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  80. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  81. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  82. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  83. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  84. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  85. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  86. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  87. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  88. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  89. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  90. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  91. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  92. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  93. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  94. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  95. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  96. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  97. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  98. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  99. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  100. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  101. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  102. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  103. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  104. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  105. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  106. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  107. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  108. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
  118. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
  119. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
  120. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
  121. package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
  122. package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  123. package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
  124. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
  125. package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
  126. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
  127. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
  128. package/lib/novel-to-game/manifest.json +508 -0
  129. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
  130. package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
  131. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
  132. package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
  133. package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
  134. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
  135. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
  136. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
  137. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
  138. package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
  139. package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
  140. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
  141. package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
  142. package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
  143. package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
  144. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
  145. package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
  146. package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
  147. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
  148. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
  149. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
  150. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
  151. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
  152. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
  153. package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
  154. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  155. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
  156. package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
  157. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
  158. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
  159. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
  160. package/lib/oh-story/manifest.json +415 -305
  161. package/lib/oh-story/roles/consistency-checker.md +1 -1
  162. package/lib/oh-story/roles/narrative-writer.md +32 -33
  163. package/lib/oh-story/roles/story-architect.md +16 -26
  164. package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
  165. package/lib/oh-story/skills/story/SKILL.md +16 -9
  166. package/lib/oh-story/skills/story/VERSION +1 -1
  167. package/lib/oh-story/skills/story/references/author-memory.md +163 -0
  168. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
  169. package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
  170. package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
  171. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
  172. package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
  173. package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
  174. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
  175. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
  176. package/lib/oh-story/skills/story-import/SKILL.md +19 -10
  177. package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
  180. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
  181. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
  182. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
  183. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
  184. package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
  185. package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
  186. package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
  187. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
  188. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
  189. package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
  190. package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
  191. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
  192. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  193. package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
  194. package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
  195. package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
  196. package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
  197. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
  198. package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
  199. package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
  200. package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
  201. package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
  202. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
  203. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  204. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
  205. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
  206. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
  207. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
  208. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
  209. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
  210. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
  211. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
  212. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
  213. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
  214. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
  215. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
  216. package/lib/oh-story/skills/story-review/SKILL.md +26 -17
  217. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
  218. package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
  219. package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
  220. package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
  221. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
  222. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
  223. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
  224. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
  225. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
  226. package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
  227. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  228. package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  229. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
  230. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
  234. package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  236. package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
  237. package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
  238. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
  239. package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
  240. package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
  241. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
  242. package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
  243. package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
  244. package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
  245. package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
  246. package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
  247. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  248. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
  249. package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
  250. package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
  251. package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
  252. package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
  253. package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
  254. package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
  255. package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  257. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  258. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  259. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  260. package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  261. package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
  262. package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
  263. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
  264. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
  265. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
  266. package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
  267. package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
  268. package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
  270. package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
  271. package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
  272. package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
  273. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
  274. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
  276. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  277. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
  278. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  279. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
  280. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
  281. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
  282. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
  283. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  284. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
  285. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
  286. package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
  287. package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
  288. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
  289. package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
  290. package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
  291. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
  292. package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
  293. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
  294. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
  295. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
  296. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
  297. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
  298. package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  299. package/lib/types/client/drama-production-view.d.ts +34 -0
  300. package/lib/types/client/drama-production-view.d.ts.map +1 -0
  301. package/lib/types/client/drama-production.d.ts +92 -0
  302. package/lib/types/client/drama-production.d.ts.map +1 -0
  303. package/lib/types/client/file-activity.d.ts +4 -2
  304. package/lib/types/client/file-activity.d.ts.map +1 -1
  305. package/lib/types/client/index.d.ts +1 -1
  306. package/lib/types/client/index.d.ts.map +1 -1
  307. package/lib/types/client/production-intents.d.ts +9 -0
  308. package/lib/types/client/production-intents.d.ts.map +1 -0
  309. package/lib/types/client/production-prompts.d.ts +9 -0
  310. package/lib/types/client/production-prompts.d.ts.map +1 -0
  311. package/lib/types/client/production-runtime.d.ts +58 -0
  312. package/lib/types/client/production-runtime.d.ts.map +1 -0
  313. package/lib/types/client/video-studio.d.ts +41 -0
  314. package/lib/types/client/video-studio.d.ts.map +1 -0
  315. package/lib/types/client/workbench-ui.d.ts +5 -0
  316. package/lib/types/client/workbench-ui.d.ts.map +1 -0
  317. package/lib/types/game-verification.d.ts +16 -0
  318. package/lib/types/game-verification.d.ts.map +1 -0
  319. package/lib/types/index.d.ts +3 -1
  320. package/lib/types/index.d.ts.map +1 -1
  321. package/lib/types/production-intent.d.ts +21 -0
  322. package/lib/types/production-intent.d.ts.map +1 -0
  323. package/lib/types/production-tool.d.ts +5 -0
  324. package/lib/types/production-tool.d.ts.map +1 -0
  325. package/lib/types/role-tool.d.ts.map +1 -1
  326. package/lib/types/skill-provider.d.ts +7 -0
  327. package/lib/types/skill-provider.d.ts.map +1 -1
  328. package/lib/types/video-project.d.ts +42 -0
  329. package/lib/types/video-project.d.ts.map +1 -0
  330. package/lib/types/workspace-request-trust.d.ts +6 -0
  331. package/lib/types/workspace-request-trust.d.ts.map +1 -1
  332. package/lib/types/workspace-route.d.ts +20 -0
  333. package/lib/types/workspace-route.d.ts.map +1 -1
  334. package/lib/video-recap/CHANGELOG.md +282 -0
  335. package/lib/video-recap/LICENSE +21 -0
  336. package/lib/video-recap/README.en.md +257 -0
  337. package/lib/video-recap/README.md +255 -0
  338. package/lib/video-recap/manifest.json +684 -0
  339. package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
  340. package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  341. package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  342. package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  343. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  344. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  345. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  346. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  347. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  348. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  349. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  350. package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  351. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  352. package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  353. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
  354. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
  355. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
  356. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
  357. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
  358. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
  359. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
  360. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
  361. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
  362. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
  363. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
  364. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
  365. package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
  366. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
  367. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
  368. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
  369. package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
  370. package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
  371. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
  372. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
  373. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
  374. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
  375. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
  376. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
  377. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
  378. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
  379. package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
  380. package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
  381. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
  382. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
  383. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
  384. package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
  385. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
  386. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
  387. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
  388. package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
  389. package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
  390. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
  391. package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
  392. package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
  393. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  394. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
  395. package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
  396. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
  397. package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
  398. package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
  399. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
  400. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
  401. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
  402. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
  403. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
  404. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
  405. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
  406. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
  407. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
  408. package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
  409. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
  410. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
  411. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
  412. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
  413. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
  414. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
  415. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
  416. package/lib/video-recap/skills/video-script/SKILL.md +248 -0
  417. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
  418. package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
  419. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
  420. package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
  421. package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
  422. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
  423. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
  424. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
  425. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
  426. package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
  427. package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
  428. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
  429. package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
  430. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
  431. package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
  432. package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
  433. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
  434. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
  435. package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
  436. package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
  437. package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
  438. package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
  439. package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
  440. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
  441. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
  442. package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
  443. package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
  444. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
  445. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
  446. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
  447. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
  448. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
  449. package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
  450. package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
  451. package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
  452. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
  453. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
  454. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
  455. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
  456. package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
  457. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
  458. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
  459. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
  460. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
  461. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
  462. package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
  463. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
  464. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
  465. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
  466. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
  467. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
  468. package/package.json +55 -40
  469. package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
  470. package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
  471. package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
  472. package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
  473. package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
  474. package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
  475. package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
  476. package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
  477. package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
  478. /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
  479. /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
@@ -0,0 +1,545 @@
1
+ """Normalize cut plans and maintain render-cache provenance."""
2
+
3
+ import hashlib
4
+
5
+ import json
6
+ import os
7
+
8
+ import re
9
+
10
+
11
+ from pathlib import Path
12
+
13
+ from lib import CONFIG, get_video_duration, log
14
+
15
+ EDITED_SOURCE_RENDER_ALGORITHM_VERSION = "edited-source-render-v3"
16
+
17
+ GEOMETRY_RENDER_ALGORITHM_VERSION = "geometry-weighted-orientation-area-fps-v2"
18
+
19
+
20
+ def parse_duration_seconds(value):
21
+ """Parse seconds, 10m/1h forms, or HH:MM:SS into seconds."""
22
+ if value is None or value == "":
23
+ return None
24
+ if isinstance(value, (int, float)):
25
+ seconds = float(value)
26
+ if seconds <= 0:
27
+ raise ValueError("duration must be positive")
28
+ return seconds
29
+
30
+ text = str(value).strip().lower()
31
+ if not text:
32
+ return None
33
+
34
+ if ":" in text:
35
+ parts = text.split(":")
36
+ if len(parts) not in (2, 3):
37
+ raise ValueError(f"invalid duration: {value}")
38
+ try:
39
+ nums = [float(p) for p in parts]
40
+ except ValueError as exc:
41
+ raise ValueError(f"invalid duration: {value}") from exc
42
+ if any(n < 0 for n in nums):
43
+ raise ValueError("duration must be positive")
44
+ if nums[-1] >= 60 or (len(nums) == 3 and nums[-2] >= 60):
45
+ raise ValueError(f"invalid duration: {value}")
46
+ if len(nums) == 2:
47
+ seconds = nums[0] * 60 + nums[1]
48
+ else:
49
+ seconds = nums[0] * 3600 + nums[1] * 60 + nums[2]
50
+ if seconds <= 0:
51
+ raise ValueError("duration must be positive")
52
+ return seconds
53
+
54
+ # One or more <number><unit> tokens: "600", "10m", "500ms", "2m30s", "1h5m30s".
55
+ # A bare number is read as seconds; units may be combined (compound durations).
56
+ factors = {"ms": 0.001, "s": 1, "m": 60, "h": 3600}
57
+ sign = 1.0
58
+ body = text
59
+ if body[:1] in "+-":
60
+ sign = -1.0 if body[0] == "-" else 1.0
61
+ body = body[1:]
62
+ token_re = re.compile(r"([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)?")
63
+ pos = 0
64
+ seconds = 0.0
65
+ matched = False
66
+ for m in token_re.finditer(body):
67
+ if m.start() != pos:
68
+ break
69
+ pos = m.end()
70
+ matched = True
71
+ seconds += float(m.group(1)) * factors[m.group(2) or "s"]
72
+ if not matched or pos != len(body):
73
+ raise ValueError(f"invalid duration: {value}")
74
+ seconds *= sign
75
+ if seconds <= 0:
76
+ raise ValueError("duration must be positive")
77
+ return seconds
78
+
79
+
80
+ def _overlaps_authored_range(ranges, start, end):
81
+ """Whether [start,end) collides with an already-accepted clip, as AUTHORED.
82
+
83
+ Overlap is judged on the agent's own in/out points, never on the padded ones.
84
+ `clip_padding` deliberately widens every clip by the same amount on both ends, so
85
+ judging padded ranges makes any two back-to-back clips (…, 10) and (10, …) look like
86
+ duplicate footage and hard-fails a perfectly ordinary plan. Padding is an output
87
+ nicety; only what the agent actually asked for defines duplication.
88
+ """
89
+ return any(start < other_end and end > other_start for other_start, other_end in ranges)
90
+
91
+
92
+ def _clip_value(raw, *names):
93
+ for name in names:
94
+ if name in raw:
95
+ return raw[name]
96
+ return None
97
+
98
+
99
+ def load_clip_plan(path):
100
+ """Load `clip_plan.json`, accepting either a list or {"clips": [...]} object."""
101
+ return json.loads(Path(path).read_text(encoding="utf-8"))
102
+
103
+
104
+ def _stable_json_dumps(value):
105
+ return json.dumps(
106
+ value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str
107
+ )
108
+
109
+
110
+ def value_fingerprint(value):
111
+ """Return a stable fingerprint for JSON-serializable non-secret values."""
112
+ return hashlib.md5(_stable_json_dumps(value).encode("utf-8")).hexdigest()
113
+
114
+
115
+ def cut_plan_fingerprint(validated_plan):
116
+ """Hash the exact normalized clip plan that determines edited_source.mp4 bytes."""
117
+ if isinstance(validated_plan, dict):
118
+ payload = dict(validated_plan)
119
+ # Provenance for raw-plan freshness is not part of the edited media bytes.
120
+ payload.pop("raw_plan_fingerprint", None)
121
+ # QC is observability derived from the media plan, not an input range decision.
122
+ payload.pop("qc", None)
123
+ else:
124
+ payload = validated_plan
125
+ return value_fingerprint(payload)
126
+
127
+
128
+ _FILE_FINGERPRINT_MEMO = {}
129
+
130
+
131
+ def _file_identity(path):
132
+ """(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
133
+ st = os.stat(os.fspath(path))
134
+ return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
135
+
136
+
137
+ def file_fingerprint(path, chunk_size=1024 * 1024):
138
+ """Return a full-content fingerprint for cache-correct identity checks.
139
+
140
+ The digest covers CONTENT only — never the path or mtime — so a copied video or
141
+ artifact is still recognised as the same asset, while any byte change invalidates
142
+ the cache even if timestamps, size, head, or tail bytes are misleading.
143
+
144
+ Identity metadata is used ONLY to memoize within a single process. One understanding
145
+ run fingerprints the same source video 8-10 times and the whole extracted frame set
146
+ 2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
147
+ any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
148
+ re-hashed, so the memo can never serve a stale digest.
149
+ """
150
+ key = _file_identity(path)
151
+ memoized = _FILE_FINGERPRINT_MEMO.get(key)
152
+ if memoized is not None:
153
+ return memoized
154
+ h = hashlib.sha256()
155
+ with open(os.fspath(path), "rb") as f:
156
+ for chunk in iter(lambda: f.read(chunk_size), b""):
157
+ h.update(chunk)
158
+ digest = h.hexdigest()
159
+ _FILE_FINGERPRINT_MEMO[key] = digest
160
+ return digest
161
+
162
+
163
+ def _edited_source_meta_path(output_path):
164
+ return Path(str(output_path) + ".meta.json")
165
+
166
+
167
+ def _load_edited_source_meta(output_path):
168
+ meta_path = _edited_source_meta_path(output_path)
169
+ if not meta_path.exists():
170
+ return None
171
+ try:
172
+ data = json.loads(meta_path.read_text(encoding="utf-8"))
173
+ except (OSError, ValueError):
174
+ return None
175
+ return data if isinstance(data, dict) else None
176
+
177
+
178
+ def _source_fingerprints_for_plan(validated_plan, input_video=None):
179
+ """Fingerprint every media file that can affect edited_source.mp4 bytes."""
180
+ paths = []
181
+ for clip in (
182
+ validated_plan.get("clips", []) if isinstance(validated_plan, dict) else []
183
+ ):
184
+ if clip.get("source_path"):
185
+ paths.append(str(clip["source_path"]))
186
+ if not paths and input_video is not None:
187
+ paths.append(str(input_video))
188
+ fingerprints = {}
189
+ for path in sorted(set(paths)):
190
+ fingerprints[str(Path(path))] = file_fingerprint(path)
191
+ return fingerprints
192
+
193
+
194
+ def edited_source_render_cache_payload():
195
+ """Render-affecting settings that invalidate edited_source.mp4 cache reuse.
196
+
197
+ Keep this payload limited to inputs/algorithms that can change rendered media bytes.
198
+ Observational QC produced after validation/render is intentionally excluded.
199
+ """
200
+ return {
201
+ "render_algorithm_version": EDITED_SOURCE_RENDER_ALGORITHM_VERSION,
202
+ "geometry_render_algorithm_version": GEOMETRY_RENDER_ALGORITHM_VERSION,
203
+ "clip_join_audio_fade_ms": round(
204
+ max(0.0, float(CONFIG.get("clip_join_audio_fade_ms", 30.0) or 0.0)), 3
205
+ ),
206
+ }
207
+
208
+
209
+ def edited_source_render_fingerprint():
210
+ return value_fingerprint(edited_source_render_cache_payload())
211
+
212
+
213
+ def _write_edited_source_meta(output_path, validated_plan, input_video=None):
214
+ meta_path = _edited_source_meta_path(output_path)
215
+ source_fingerprints = _source_fingerprints_for_plan(validated_plan, input_video)
216
+ has_plan_sources = any(
217
+ clip.get("source_path")
218
+ for clip in (
219
+ validated_plan.get("clips", []) if isinstance(validated_plan, dict) else []
220
+ )
221
+ )
222
+ legacy_source_fp = (
223
+ file_fingerprint(input_video)
224
+ if input_video is not None
225
+ and not has_plan_sources
226
+ and len(source_fingerprints) == 1
227
+ else None
228
+ )
229
+ meta = {
230
+ "schema_version": 2,
231
+ "clip_plan_fingerprint": cut_plan_fingerprint(validated_plan),
232
+ "render_fingerprint": edited_source_render_fingerprint(),
233
+ "render_cache": edited_source_render_cache_payload(),
234
+ "source_fingerprints": source_fingerprints,
235
+ "edited_source_fingerprint": file_fingerprint(output_path),
236
+ "total_duration": validated_plan.get("total_duration"),
237
+ "clip_count": len(validated_plan.get("clips", [])),
238
+ }
239
+ delivery_qc = (validated_plan.get("qc") or {}).get("delivery_qc")
240
+ if delivery_qc:
241
+ meta["delivery_qc"] = delivery_qc
242
+ # Preserve the legacy key for existing single-source callers/tests/metadata readers.
243
+ if legacy_source_fp is not None:
244
+ meta["source_video_fingerprint"] = legacy_source_fp
245
+ meta_path.write_text(
246
+ json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8"
247
+ )
248
+
249
+
250
+ def should_reuse_edited_source(output_path, validated_plan, input_video=None):
251
+ """Return True only when edited_source.mp4 matches source media and cut params."""
252
+ output_path = Path(output_path)
253
+ if not output_path.exists():
254
+ return False
255
+ meta = _load_edited_source_meta(output_path)
256
+ if not meta or meta.get("clip_plan_fingerprint") != cut_plan_fingerprint(
257
+ validated_plan
258
+ ):
259
+ return False
260
+ if meta.get("render_fingerprint") != edited_source_render_fingerprint():
261
+ return False
262
+ expected_sources = _source_fingerprints_for_plan(validated_plan, input_video)
263
+ meta_sources = meta.get("source_fingerprints")
264
+ if meta_sources is None and input_video is not None:
265
+ meta_sources = {str(Path(input_video)): meta.get("source_video_fingerprint")}
266
+ return bool(
267
+ meta_sources == expected_sources
268
+ and meta.get("edited_source_fingerprint") == file_fingerprint(output_path)
269
+ )
270
+
271
+
272
+ def _manifest_source_entries(sources_manifest):
273
+ """Return source rows from common multi-source manifest shapes."""
274
+ if isinstance(sources_manifest, dict):
275
+ if isinstance(sources_manifest.get("sources"), list):
276
+ return sources_manifest["sources"]
277
+ rows = []
278
+ for source_id, value in sources_manifest.items():
279
+ if source_id in {"schema_version", "version"}:
280
+ continue
281
+ if isinstance(value, dict):
282
+ row = dict(value)
283
+ row.setdefault("source_id", source_id)
284
+ rows.append(row)
285
+ if rows:
286
+ return rows
287
+ elif isinstance(sources_manifest, list):
288
+ return sources_manifest
289
+ raise ValueError(
290
+ "sources manifest must be a list, a {sources:[...]} object, or a source_id map"
291
+ )
292
+
293
+
294
+ def normalize_sources_manifest(sources_manifest):
295
+ """Normalize source manifest rows to {source_id: {source_path, duration}}."""
296
+ sources = {}
297
+ for idx, raw in enumerate(_manifest_source_entries(sources_manifest)):
298
+ if not isinstance(raw, dict):
299
+ raise ValueError(f"source #{idx + 1} must be an object")
300
+ source_id = raw.get("source_id", raw.get("id", raw.get("name")))
301
+ if source_id in (None, ""):
302
+ raise ValueError(f"source #{idx + 1} is missing source_id")
303
+ source_id = str(source_id)
304
+ source_path = raw.get(
305
+ "source_path",
306
+ raw.get("path", raw.get("video_path", raw.get("video", raw.get("file")))),
307
+ )
308
+ if not source_path:
309
+ raise ValueError(f"source {source_id} is missing source_path/path")
310
+ duration = raw.get(
311
+ "duration", raw.get("duration_seconds", raw.get("source_duration"))
312
+ )
313
+ if duration in (None, ""):
314
+ duration = get_video_duration(source_path)
315
+ try:
316
+ duration = max(0.0, float(duration or 0.0))
317
+ except (TypeError, ValueError) as exc:
318
+ raise ValueError(f"source {source_id} has invalid duration") from exc
319
+ sources[source_id] = {
320
+ "source_id": source_id,
321
+ "source_path": str(source_path),
322
+ "duration": duration,
323
+ }
324
+ if raw.get("source_work_dir") not in (None, ""):
325
+ sources[source_id]["source_work_dir"] = str(raw["source_work_dir"])
326
+ if not sources:
327
+ raise ValueError("sources manifest has no sources")
328
+ return sources
329
+
330
+
331
+ def normalize_multi_source_clip_plan(
332
+ raw_plan,
333
+ sources_manifest,
334
+ target_duration=None,
335
+ clip_padding=0.0,
336
+ min_clip_duration=0.3,
337
+ allow_overlap=False,
338
+ ):
339
+ """Validate a multi-source clip plan and map source_id clips to source paths/durations.
340
+
341
+ Clip order follows the raw plan; overlap validation is isolated per source_id.
342
+ """
343
+ sources = normalize_sources_manifest(sources_manifest)
344
+ if isinstance(raw_plan, dict):
345
+ raw_clips = raw_plan.get("clips", [])
346
+ plan_target = raw_plan.get("target_duration") or raw_plan.get(
347
+ "target_duration_seconds"
348
+ )
349
+ if target_duration is None and plan_target not in (None, ""):
350
+ target_duration = parse_duration_seconds(plan_target)
351
+ elif isinstance(raw_plan, list):
352
+ raw_clips = raw_plan
353
+ else:
354
+ raise ValueError(
355
+ "clip_plan.json must be a JSON array or an object with a clips array"
356
+ )
357
+
358
+ if not isinstance(raw_clips, list):
359
+ raise ValueError("clip_plan.json field `clips` must be an array")
360
+
361
+ padding = max(0.0, float(clip_padding or 0.0))
362
+ min_duration = max(0.05, float(min_clip_duration or 0.05))
363
+ clips = []
364
+ source_ranges = {}
365
+ cursor = 0.0
366
+
367
+ for idx, raw in enumerate(raw_clips):
368
+ if not isinstance(raw, dict):
369
+ log(f" 跳过无效 clip #{idx + 1}: not an object")
370
+ continue
371
+ source_id = raw.get("source_id", raw.get("id"))
372
+ if source_id in (None, ""):
373
+ raise ValueError(f"clip #{idx + 1} is missing source_id")
374
+ source_id = str(source_id)
375
+ source = sources.get(source_id)
376
+ if not source:
377
+ raise ValueError(
378
+ f"clip #{idx + 1} references unknown source_id: {source_id}"
379
+ )
380
+ try:
381
+ raw_start = float(_clip_value(raw, "start", "source_start", "in"))
382
+ raw_end = float(_clip_value(raw, "end", "source_end", "out"))
383
+ except (TypeError, ValueError):
384
+ log(f" 跳过无效 clip #{idx + 1}: missing numeric start/end")
385
+ continue
386
+ if raw_end - raw_start < min_duration:
387
+ log(f" 跳过过短 clip #{idx + 1}: {raw_start:.1f}-{raw_end:.1f}s")
388
+ continue
389
+ source_duration = source["duration"]
390
+ start = round(max(0.0, min(raw_start - padding, source_duration)), 3)
391
+ end = round(max(0.0, min(raw_end + padding, source_duration)), 3)
392
+ if end - start < min_duration:
393
+ log(f" 跳过过短 clip #{idx + 1}: {start:.1f}-{end:.1f}s")
394
+ continue
395
+ ranges = source_ranges.setdefault(source_id, [])
396
+ if not allow_overlap and _overlaps_authored_range(ranges, raw_start, raw_end):
397
+ raise ValueError(
398
+ f"clip #{idx + 1} overlaps an earlier source range for source_id {source_id}; "
399
+ "split or remove duplicate source footage before mapping narration"
400
+ )
401
+ ranges.append((raw_start, raw_end))
402
+
403
+ duration = round(end - start, 3)
404
+ clip = {
405
+ "clip_id": len(clips),
406
+ "source_id": source_id,
407
+ "source_path": source["source_path"],
408
+ "source_start": start,
409
+ "source_end": end,
410
+ "output_start": round(cursor, 3),
411
+ "output_end": round(cursor + duration, 3),
412
+ "duration": duration,
413
+ "reason": str(raw.get("reason", raw.get("note", ""))).strip(),
414
+ }
415
+ clips.append(clip)
416
+ cursor += duration
417
+
418
+ if not clips:
419
+ raise ValueError("clip_plan.json has no valid clips")
420
+
421
+ total_duration = round(sum(c["duration"] for c in clips), 3)
422
+ plan = {
423
+ "clips": clips,
424
+ "total_duration": total_duration,
425
+ "target_duration": round(float(target_duration), 3)
426
+ if target_duration
427
+ else None,
428
+ "sources": {
429
+ sid: {
430
+ "source_path": s["source_path"],
431
+ "duration": round(s["duration"], 3),
432
+ **(
433
+ {"source_work_dir": s["source_work_dir"]}
434
+ if s.get("source_work_dir")
435
+ else {}
436
+ ),
437
+ }
438
+ for sid, s in sources.items()
439
+ },
440
+ "allow_overlap": bool(allow_overlap),
441
+ }
442
+ if target_duration and total_duration > target_duration * 1.15:
443
+ plan["warning"] = (
444
+ f"validated clips total {total_duration:.1f}s exceeds target "
445
+ f"{float(target_duration):.1f}s by more than 15%"
446
+ )
447
+ log(f"警告: {plan['warning']}")
448
+ return plan
449
+
450
+
451
+ def normalize_clip_plan(
452
+ raw_plan,
453
+ video_duration,
454
+ target_duration=None,
455
+ clip_padding=0.0,
456
+ min_clip_duration=0.3,
457
+ allow_overlap=False,
458
+ ):
459
+ """Validate and enrich an agent-authored clip plan.
460
+
461
+ Returns a dict with validated `clips`, `total_duration`, and target metadata.
462
+ Clip order follows the agent-provided order, so montage ordering is possible.
463
+ """
464
+ if isinstance(raw_plan, dict):
465
+ raw_clips = raw_plan.get("clips", [])
466
+ plan_target = raw_plan.get("target_duration") or raw_plan.get(
467
+ "target_duration_seconds"
468
+ )
469
+ if target_duration is None and plan_target not in (None, ""):
470
+ target_duration = parse_duration_seconds(plan_target)
471
+ elif isinstance(raw_plan, list):
472
+ raw_clips = raw_plan
473
+ else:
474
+ raise ValueError(
475
+ "clip_plan.json must be a JSON array or an object with a clips array"
476
+ )
477
+
478
+ if not isinstance(raw_clips, list):
479
+ raise ValueError("clip_plan.json field `clips` must be an array")
480
+
481
+ video_duration = max(0.0, float(video_duration or 0.0))
482
+ padding = max(0.0, float(clip_padding or 0.0))
483
+ min_duration = max(0.05, float(min_clip_duration or 0.05))
484
+ clips = []
485
+ source_ranges = []
486
+ cursor = 0.0
487
+
488
+ for idx, raw in enumerate(raw_clips):
489
+ if not isinstance(raw, dict):
490
+ log(f" 跳过无效 clip #{idx + 1}: not an object")
491
+ continue
492
+ try:
493
+ raw_start = float(_clip_value(raw, "start", "source_start", "in"))
494
+ raw_end = float(_clip_value(raw, "end", "source_end", "out"))
495
+ except (TypeError, ValueError):
496
+ log(f" 跳过无效 clip #{idx + 1}: missing numeric start/end")
497
+ continue
498
+ if raw_end - raw_start < min_duration:
499
+ log(f" 跳过过短 clip #{idx + 1}: {raw_start:.1f}-{raw_end:.1f}s")
500
+ continue
501
+ start = round(max(0.0, min(raw_start - padding, video_duration)), 3)
502
+ end = round(max(0.0, min(raw_end + padding, video_duration)), 3)
503
+ if end - start < min_duration:
504
+ log(f" 跳过过短 clip #{idx + 1}: {start:.1f}-{end:.1f}s")
505
+ continue
506
+ if not allow_overlap and _overlaps_authored_range(source_ranges, raw_start, raw_end):
507
+ raise ValueError(
508
+ f"clip #{idx + 1} overlaps an earlier source range; "
509
+ "split or remove duplicate source footage before mapping narration"
510
+ )
511
+ source_ranges.append((raw_start, raw_end))
512
+
513
+ duration = round(end - start, 3)
514
+ clip = {
515
+ "clip_id": len(clips),
516
+ "source_start": start,
517
+ "source_end": end,
518
+ "output_start": round(cursor, 3),
519
+ "output_end": round(cursor + duration, 3),
520
+ "duration": duration,
521
+ "reason": str(raw.get("reason", raw.get("note", ""))).strip(),
522
+ }
523
+ clips.append(clip)
524
+ cursor += duration
525
+
526
+ if not clips:
527
+ raise ValueError("clip_plan.json has no valid clips")
528
+
529
+ total_duration = round(sum(c["duration"] for c in clips), 3)
530
+ plan = {
531
+ "clips": clips,
532
+ "total_duration": total_duration,
533
+ "target_duration": round(float(target_duration), 3)
534
+ if target_duration
535
+ else None,
536
+ "source_duration": round(video_duration, 3),
537
+ "allow_overlap": bool(allow_overlap),
538
+ }
539
+ if target_duration and total_duration > target_duration * 1.15:
540
+ plan["warning"] = (
541
+ f"validated clips total {total_duration:.1f}s exceeds target "
542
+ f"{float(target_duration):.1f}s by more than 15%"
543
+ )
544
+ log(f"警告: {plan['warning']}")
545
+ return plan