@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,705 @@
1
+ import base64
2
+ import json
3
+ import mimetypes
4
+ import re
5
+ from collections import OrderedDict
6
+ from pathlib import Path
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from threading import Lock
9
+
10
+ from extract import (
11
+ FRAME_TIME_CONVENTION_VERSION,
12
+ frame_time,
13
+ parse_frame_number,
14
+ )
15
+ from lib import CONFIG
16
+ from lib import log, api_call, load_prompt, mimo_video_api_call, run_cmd, file_fingerprint, stable_hash
17
+
18
+ # ── Step 4: VLM 视觉分析 ─────────────────────────────────────────────
19
+
20
+ def _parse_vlm_depth_response(raw_text):
21
+ """解析 VLM 深度分析响应,提取【描述】、【帧标签】和【深层分析】"""
22
+ if not raw_text or not raw_text.strip():
23
+ return "(VLM 无法识别此场景画面)", "", {}
24
+
25
+ # 提取【描述】
26
+ desc_match = re.search(r'【描述】\s*\n?(.*?)(?=【帧标签】|【深层分析】|$)', raw_text, re.DOTALL)
27
+ if desc_match:
28
+ description = desc_match.group(1).strip()
29
+ else:
30
+ description = raw_text.strip()
31
+
32
+ # 提取【帧标签】
33
+ frame_facts = {}
34
+ facts_match = re.search(r'【帧标签】\s*\n?(.*?)(?=【深层分析】|$)', raw_text, re.DOTALL)
35
+ if facts_match:
36
+ for line in facts_match.group(1).strip().split("\n"):
37
+ line = line.strip()
38
+ if not line:
39
+ continue
40
+ # 格式: "12.0s | 男子拿起茶壶对嘴喝, 满脸疲惫"
41
+ m = re.match(r'([\d.]+)\s*s?\s*\|\s*(.+)', line)
42
+ if m:
43
+ ts = m.group(1)
44
+ actions = [a.strip() for a in re.split(r"[,,;;、]+", m.group(2)) if a.strip()]
45
+ if actions:
46
+ frame_facts[ts] = actions
47
+
48
+ # 提取【深层分析】
49
+ depth_match = re.search(r'【深层分析】\s*\n?(.*?)$', raw_text, re.DOTALL)
50
+ depth_analysis = depth_match.group(1).strip() if depth_match else ""
51
+
52
+ if not description:
53
+ description = "(VLM 无法识别此场景画面)"
54
+
55
+ return description, depth_analysis, frame_facts
56
+
57
+
58
+ def _max_frames_for_duration(duration):
59
+ """Frames the VLM sees for one scene: ~1 per `vlm_seconds_per_frame`, floor 3, capped by
60
+ `vlm_max_frames`. Replaces the old hard cap of 6 that starved long/merged scenes."""
61
+ spf = float(CONFIG.get("vlm_seconds_per_frame", 4.0) or 4.0)
62
+ ceiling = int(CONFIG.get("vlm_max_frames", 16) or 16)
63
+ return max(3, min(ceiling, round(max(0.0, float(duration)) / spf)))
64
+
65
+
66
+ def _vlm_scene_cache_path(work_dir):
67
+ return Path(work_dir) / "vlm_scene_cache.json"
68
+
69
+
70
+ def _load_vlm_scene_cache(work_dir):
71
+ """Per-scene VLM resume cache (scene_key -> analysis). Tolerant: {} if absent/corrupt."""
72
+ path = _vlm_scene_cache_path(work_dir)
73
+ if not path.exists():
74
+ return {}
75
+ try:
76
+ data = json.loads(path.read_text(encoding="utf-8"))
77
+ return data if isinstance(data, dict) else {}
78
+ except (OSError, ValueError):
79
+ return {}
80
+
81
+
82
+ def _flush_vlm_scene_cache(work_dir, cache):
83
+ """Persist the resume cache atomically (temp + rename) so an abort/crash never corrupts it."""
84
+ path = _vlm_scene_cache_path(work_dir)
85
+ try:
86
+ tmp = path.with_name(path.name + ".tmp")
87
+ tmp.write_text(json.dumps(cache, ensure_ascii=False), encoding="utf-8")
88
+ tmp.replace(path)
89
+ except OSError as exc:
90
+ log(f"VLM 场景缓存写入失败(忽略): {exc}")
91
+
92
+
93
+ def _looks_rate_limited(message):
94
+ """Heuristic: was a scene failure a transient rate-limit (worth a low-concurrency retry) rather
95
+ than a persistent error (empty response / parse failure) that re-running would not fix?"""
96
+ m = str(message).lower()
97
+ return "429" in m or "too many requests" in m or "rate limit" in m or "限流" in m
98
+
99
+
100
+ def analyze_scenes(scenes, frames, work_dir, *, resume=True):
101
+ """对每个场景的关键帧调用 VLM 进行视觉分析(并行)"""
102
+ if not scenes:
103
+ analyses = []
104
+ vlm_file = work_dir / "vlm_analysis.json"
105
+ vlm_file.write_text(json.dumps(analyses, ensure_ascii=False, indent=2), encoding="utf-8")
106
+ log("VLM 分析完成: 0 个场景")
107
+ return analyses
108
+
109
+ if not frames:
110
+ raise RuntimeError("VLM 分析需要先提取至少一帧;frames 为空")
111
+
112
+ fps = CONFIG["fps"]
113
+ if fps <= 0:
114
+ raise ValueError("CONFIG['fps'] 必须大于 0;请先运行完整 pipeline 或指定 --fps")
115
+
116
+ vlm_prompt = load_prompt("VLM_DEPTH_PROMPT")
117
+ if not vlm_prompt:
118
+ vlm_prompt = "仔细观察这些视频帧。分两部分输出:\n【描述】不超过80字,描述画面中正在发生什么。\n【深层分析】不超过120字,分析角色情绪、关系动态、潜台词。"
119
+
120
+ ctx = CONFIG.get("context_info", "")
121
+ if ctx:
122
+ vlm_prompt = f"已知信息:{ctx}\n\n{vlm_prompt}"
123
+
124
+ # 构建帧时间映射 (frame_NNNNN.jpg -> time in seconds)。换算规则由 extract.py 独家定义:
125
+ # ffmpeg 首帧在 t=0 而文件编号从 1 起,所以是 (n-1)/fps,不是 n/fps。
126
+ frame_times = {}
127
+ for f in frames:
128
+ num = parse_frame_number(f)
129
+ if num is None:
130
+ continue
131
+ frame_times[f] = frame_time(num, fps)
132
+
133
+ # base64 编码缓存(LRU)。原来是无上界的 dict:整个 VLM 阶段会把用到的每一帧的 base64
134
+ # 都常驻内存,而 base64 比原图还大 1/3。40 分钟视频 fps=1 约 2400 帧 → 数百 MB 常驻。
135
+ # 相邻场景才会复用同一帧,容量取「并发数 × 单场景最大帧数」就够,再多也命中不了。
136
+ b64_capacity = max(
137
+ 8,
138
+ int(CONFIG.get("vlm_max_frames", 16) or 16) * max(1, int(CONFIG.get("vlm_workers", 4) or 4)),
139
+ )
140
+ b64_cache = OrderedDict()
141
+ b64_lock = Lock()
142
+
143
+ def _get_b64(frame_path):
144
+ with b64_lock:
145
+ cached = b64_cache.get(frame_path)
146
+ if cached is not None:
147
+ b64_cache.move_to_end(frame_path)
148
+ return cached
149
+ encoded = base64.b64encode(frame_path.read_bytes()).decode()
150
+ with b64_lock:
151
+ b64_cache[frame_path] = encoded
152
+ b64_cache.move_to_end(frame_path)
153
+ while len(b64_cache) > b64_capacity:
154
+ b64_cache.popitem(last=False)
155
+ return encoded
156
+
157
+ def _analyze_single_scene(i, scene):
158
+ """分析单个场景,返回 (scene_id, result_dict)"""
159
+ scene_frames = [f for f, t in frame_times.items()
160
+ if scene["start"] <= t <= scene["end"]]
161
+ if not scene_frames:
162
+ mid = (scene["start"] + scene["end"]) / 2
163
+ scene_frames = [min(frames, key=lambda f: abs(frame_times.get(f, 999) - mid))]
164
+
165
+ duration = scene["end"] - scene["start"]
166
+ max_frames = _max_frames_for_duration(duration)
167
+
168
+ if len(scene_frames) > max_frames:
169
+ step = len(scene_frames) / max_frames
170
+ scene_frames = [scene_frames[int(j * step)] for j in range(max_frames)]
171
+ else:
172
+ scene_frames = scene_frames[:max_frames]
173
+
174
+ content_parts = []
175
+ for f in scene_frames:
176
+ b64 = _get_b64(f)
177
+ content_parts.append({
178
+ "type": "image_url",
179
+ "image_url": {"url": f"data:image/jpeg;base64,{b64}"}
180
+ })
181
+
182
+ # 帧级事实标签:将帧时间注入prompt
183
+ frame_ts_list = [f"{frame_times[f]:.1f}s" for f in scene_frames]
184
+ frame_ts_text = "帧时间点: " + ", ".join(frame_ts_list)
185
+ content_parts.append({"type": "text", "text": frame_ts_text + "\n\n" + vlm_prompt})
186
+
187
+ payload = {
188
+ "model": CONFIG["vlm_model"],
189
+ "messages": [{"role": "user", "content": content_parts}],
190
+ "max_tokens": int(CONFIG.get("vlm_max_tokens", 1500) or 1500),
191
+ }
192
+
193
+ log(f"VLM 分析场景 {i+1}/{len(scenes)} ({len(scene_frames)} 帧)...")
194
+
195
+ raw_response = ""
196
+ for attempt in range(3):
197
+ resp = api_call(payload)
198
+ try:
199
+ msg = resp["choices"][0]["message"]
200
+ raw_response = (msg.get("content") or msg.get("reasoning_content") or "")
201
+ except (KeyError, IndexError):
202
+ log(f"VLM 返回异常: {json.dumps(resp, ensure_ascii=False)[:200]}")
203
+
204
+ if raw_response.strip():
205
+ break
206
+
207
+ if attempt < 2:
208
+ log(f" 场景 {i+1} VLM 返回空,重试 ({attempt+2}/3)...")
209
+ retry_parts = content_parts[:-1]
210
+ retry_parts.append({"type": "text", "text": frame_ts_text + "\n\n" + vlm_prompt + "\n请务必按格式输出,不要留空。"})
211
+ payload = {
212
+ "model": CONFIG["vlm_model"],
213
+ "messages": [{"role": "user", "content": retry_parts}],
214
+ "max_tokens": int(CONFIG.get("vlm_max_tokens", 1500) or 1500),
215
+ }
216
+
217
+ if not raw_response.strip():
218
+ raise RuntimeError("VLM 连续 3 次返回空内容")
219
+
220
+ # 解析 【描述】、【帧标签】和【深层分析】
221
+ description, depth_analysis, frame_facts = _parse_vlm_depth_response(raw_response)
222
+
223
+ result = {
224
+ "scene_id": i,
225
+ "start": scene["start"],
226
+ "end": scene["end"],
227
+ "description": description,
228
+ "depth_analysis": depth_analysis,
229
+ }
230
+ if frame_facts:
231
+ result["frame_facts"] = frame_facts
232
+
233
+ return i, result
234
+
235
+ # 断点续传:逐场景持久化分析结果,避免少数场景失败(多为 429 限流)时整批画面理解全部作废。
236
+ cache = _load_vlm_scene_cache(work_dir) if resume else {}
237
+ prompt_fp = stable_hash(vlm_prompt)
238
+
239
+ def _scene_cache_key(i, scene):
240
+ # Must invalidate on the SAME output-affecting settings the outer stage gate tracks
241
+ # (understand.py:_vlm_cache_payload). prompt_fp already covers vlm_prompt + context_info
242
+ # (which folds in background_research). The three below change the request/endpoint but
243
+ # are NOT in vlm_prompt, so they must be keyed explicitly — otherwise a partial-failure
244
+ # cache reused after flipping e.g. mimo_disable_thinking yields a stale/mixed analysis.
245
+ return "|".join(str(x) for x in (
246
+ i, round(float(scene["start"]), 3), round(float(scene["end"]), 3),
247
+ CONFIG.get("vlm_model"), prompt_fp, CONFIG.get("vlm_max_tokens"),
248
+ CONFIG.get("vlm_seconds_per_frame"), CONFIG.get("vlm_max_frames"),
249
+ round(float(fps), 3), FRAME_TIME_CONVENTION_VERSION,
250
+ CONFIG.get("api_url"), CONFIG.get("mimo_disable_thinking", True),
251
+ CONFIG.get("mimo_media_resolution"),
252
+ ))
253
+
254
+ analyses = [None] * len(scenes)
255
+ todo = []
256
+ for i, s in enumerate(scenes):
257
+ cached = cache.get(_scene_cache_key(i, s))
258
+ analyses[i] = cached if isinstance(cached, dict) else None
259
+ if analyses[i] is None:
260
+ todo.append(i)
261
+ if todo and len(todo) < len(scenes):
262
+ log(f"VLM 复用 {len(scenes) - len(todo)} 个已缓存场景,待分析 {len(todo)} 个")
263
+
264
+ def _run_pass(indices, workers):
265
+ """分析给定场景索引;结果按批持久化到续传缓存(在主线程,无需加锁)。返回 (i, err) 失败列表。
266
+
267
+ 续传缓存每次都是整份重写,原来每完成一个场景就刷一次 → 写入量随场景数平方增长
268
+ (300 个场景约 75MB 冗余写),而且序列化发生在收结果的主循环上。改成按批刷:
269
+ 崩溃最多丢失不到一批的进度,而这些场景本来就会在重跑时被重新分析。
270
+ """
271
+ if not indices:
272
+ return []
273
+ failures = []
274
+ pending = 0
275
+ # 与并发度对齐:一批大致就是「同时在飞的那几个场景」。
276
+ flush_every = max(1, min(16, workers))
277
+ with ThreadPoolExecutor(max_workers=max(1, workers)) as executor:
278
+ futures = {executor.submit(_analyze_single_scene, i, scenes[i]): i for i in indices}
279
+ try:
280
+ for future in as_completed(futures):
281
+ i = futures[future]
282
+ try:
283
+ idx, result = future.result()
284
+ analyses[idx] = result
285
+ cache[_scene_cache_key(idx, scenes[idx])] = result
286
+ pending += 1
287
+ if pending >= flush_every:
288
+ _flush_vlm_scene_cache(work_dir, cache)
289
+ pending = 0
290
+ except Exception as e: # noqa: BLE001 - 单个场景失败不能拖垮其余已完成的
291
+ log(f"VLM 场景 {i+1} 分析失败: {e}")
292
+ failures.append((i, str(e)))
293
+ finally:
294
+ # 无论正常结束、失败还是中断,都把这一轮已完成的场景落盘。
295
+ if pending:
296
+ _flush_vlm_scene_cache(work_dir, cache)
297
+ return failures
298
+
299
+ base_workers = min(len(todo) or 1, int(CONFIG.get("vlm_workers", 4) or 4))
300
+ log(f"VLM 并行分析 {len(todo)} 个场景 (workers={base_workers})...")
301
+ failures = _run_pass(todo, base_workers)
302
+ if failures:
303
+ # 限流(429)多是并发瞬时拥塞,降并发重试一轮(已成功的从缓存跳过);其它错误(空响应/解析失败)
304
+ # 重试无益,直接保留为失败,不浪费一轮调用。
305
+ rate_limited = [i for i, msg in failures if _looks_rate_limited(msg)]
306
+ persistent = [(i, msg) for i, msg in failures if not _looks_rate_limited(msg)]
307
+ if rate_limited:
308
+ retry_workers = max(1, base_workers // 4)
309
+ log(f"VLM {len(rate_limited)} 个场景疑似限流(429),降并发到 {retry_workers} 重试...")
310
+ persistent += _run_pass(rate_limited, retry_workers)
311
+ failures = persistent
312
+
313
+ if failures:
314
+ sample = "; ".join(f"场景 {i+1}: {msg}" for i, msg in failures[:3])
315
+ raise RuntimeError(
316
+ f"VLM 分析失败 {len(failures)}/{len(scenes)} 个场景。其余已缓存到 vlm_scene_cache.json,"
317
+ f"重跑可断点续传(只重试失败场景)。示例: {sample}"
318
+ )
319
+
320
+ vlm_file = work_dir / "vlm_analysis.json"
321
+ vlm_file.write_text(json.dumps(analyses, ensure_ascii=False, indent=2), encoding="utf-8")
322
+ _vlm_scene_cache_path(work_dir).unlink(missing_ok=True) # 完整理解已生成,续传缓存可清理
323
+ log(f"VLM 分析完成: {len(analyses)} 个场景")
324
+ return analyses
325
+
326
+
327
+ def _video_data_url(video_path):
328
+ """Return a MiMo-compatible data URL for a local video chunk, or None when too large."""
329
+ max_bytes = int(float(CONFIG.get("mimo_video_base64_max_mb", 45.0)) * 1024 * 1024)
330
+ encoded_size = int(video_path.stat().st_size * 4 / 3) + 128
331
+ if encoded_size > max_bytes:
332
+ log(
333
+ "MiMo 视频分片超过 base64 上限: "
334
+ f"编码后约 {encoded_size / 1024 / 1024:.1f}MB,超过限制 {max_bytes / 1024 / 1024:.1f}MB;"
335
+ "请降低 MIMO_VIDEO_CHUNK_MAX_SECONDS 或 MIMO_VIDEO_FPS"
336
+ )
337
+ return None
338
+ mime_type = mimetypes.guess_type(str(video_path))[0] or "video/mp4"
339
+ encoded = base64.b64encode(video_path.read_bytes()).decode("ascii")
340
+ return f"data:{mime_type};base64,{encoded}"
341
+
342
+
343
+ def _mimo_video_chunks(scenes):
344
+ """Build local MiMo video-understanding chunks from ffmpeg scene boundaries."""
345
+ if not scenes:
346
+ raise RuntimeError("MiMo 视频分片理解需要 scenes;请先运行 ffmpeg scene/scdet 场景检测")
347
+
348
+ max_seconds = float(CONFIG.get("mimo_video_chunk_max_seconds", 20.0) or 20.0)
349
+ min_seconds = float(CONFIG.get("mimo_video_chunk_min_seconds", 1.0) or 1.0)
350
+ chunks = []
351
+ for scene_index, scene in enumerate(scenes):
352
+ try:
353
+ start = float(scene.get("start", 0.0))
354
+ end = float(scene.get("end", start))
355
+ except (TypeError, ValueError, AttributeError):
356
+ continue
357
+ if end <= start:
358
+ continue
359
+ scene_id = scene.get("scene_id", scene_index) if isinstance(scene, dict) else scene_index
360
+ cursor = start
361
+ while cursor < end:
362
+ chunk_end = min(end, cursor + max_seconds)
363
+ if end - chunk_end < min_seconds and chunk_end < end:
364
+ chunk_end = end
365
+ if chunk_end > cursor:
366
+ chunks.append({
367
+ "chunk_id": len(chunks),
368
+ "scene_id": scene_id,
369
+ "start": round(cursor, 3),
370
+ "end": round(chunk_end, 3),
371
+ })
372
+ cursor = chunk_end
373
+ if not chunks:
374
+ raise RuntimeError("MiMo 视频分片理解没有可用分片;请检查 scenes.json")
375
+ return chunks
376
+
377
+
378
+ def _extract_video_chunk(video_path, chunk, output_path):
379
+ """Cut one scene-based chunk into a compact local MP4 for MiMo video_url data URL."""
380
+ start = float(chunk["start"])
381
+ duration = max(0.1, float(chunk["end"]) - start)
382
+ fps = float(CONFIG.get("mimo_video_fps", 2.0) or 2.0)
383
+ cmd = [
384
+ "ffmpeg", "-y",
385
+ "-ss", f"{start:.3f}",
386
+ "-t", f"{duration:.3f}",
387
+ "-i", str(video_path),
388
+ "-map", "0:v:0",
389
+ "-an",
390
+ "-vf", f"fps={fps:g}",
391
+ "-c:v", "libx264",
392
+ "-preset", "veryfast",
393
+ "-crf", "30",
394
+ "-pix_fmt", "yuv420p",
395
+ "-movflags", "+faststart",
396
+ str(output_path),
397
+ ]
398
+ result = run_cmd(cmd, timeout=CONFIG.get("mimo_video_chunk_timeout", 180))
399
+ if result.returncode != 0:
400
+ raise RuntimeError(f"MiMo 视频分片裁剪失败: {result.stderr[-500:]}")
401
+ return output_path
402
+
403
+
404
+ def _mimo_chunk_prompt(chunk):
405
+ return (
406
+ f"这是原视频 {chunk['start']:.1f}s-{chunk['end']:.1f}s 的场景分片,"
407
+ f"scene_id={chunk['scene_id']}。"
408
+ f"{CONFIG.get('mimo_video_prompt', '请用中文概括这个视频分片。')}"
409
+ )
410
+
411
+
412
+ def _mimo_video_model():
413
+ """MiMo 视频理解使用的模型:优先 mimo_video_model,回退 mimo_model,再回退 vlm_model。"""
414
+ return CONFIG.get("mimo_video_model") or CONFIG.get("mimo_model") or CONFIG["vlm_model"]
415
+
416
+
417
+ def mimo_video_settings_fingerprint():
418
+ """Return non-secret MiMo video-overview settings that affect generated content."""
419
+ return {
420
+ "model": _mimo_video_model(),
421
+ "mimo_video_api_url": CONFIG.get("mimo_video_api_url"),
422
+ "mimo_video_fps": CONFIG.get("mimo_video_fps", 2.0),
423
+ "mimo_media_resolution": CONFIG.get("mimo_media_resolution", "default"),
424
+ "mimo_video_chunk_max_seconds": CONFIG.get("mimo_video_chunk_max_seconds", 20.0),
425
+ "mimo_video_chunk_min_seconds": CONFIG.get("mimo_video_chunk_min_seconds", 1.0),
426
+ "mimo_video_base64_max_mb": CONFIG.get("mimo_video_base64_max_mb", 45.0),
427
+ "mimo_video_prompt": CONFIG.get("mimo_video_prompt", ""),
428
+ "mimo_disable_thinking": CONFIG.get("mimo_disable_thinking", True),
429
+ }
430
+
431
+
432
+ def _mimo_chunk_cache_key(chunk):
433
+ """Stable identifier for a MiMo chunk (index + scene span) for partial-cache reuse."""
434
+ return (
435
+ f"{chunk['chunk_id']}|{chunk['scene_id']}|"
436
+ f"{float(chunk['start']):.3f}-{float(chunk['end']):.3f}"
437
+ )
438
+
439
+
440
+ def _mimo_cached_chunks_fingerprint(done):
441
+ return stable_hash(done)
442
+
443
+
444
+ def _mimo_overview_payload_fingerprint(overview):
445
+ payload = dict(overview)
446
+ payload.pop("overview_fingerprint", None)
447
+ return stable_hash(payload)
448
+
449
+
450
+ def _mimo_partial_provenance(video_path, scenes):
451
+ return {
452
+ "source_video_fingerprint": file_fingerprint(video_path),
453
+ "chunks": [_mimo_chunk_cache_key(chunk) for chunk in _mimo_video_chunks(scenes)],
454
+ }
455
+
456
+
457
+ def _load_mimo_partial(partial_path, video_path=None, scenes=None):
458
+ """Load the internal partial chunk cache, keyed by chunk identifier.
459
+
460
+ Returns {} when missing/unreadable or when settings/source/chunk provenance differs,
461
+ so a changed source video or scene plan cannot reuse paid chunks from another run.
462
+ """
463
+ if not partial_path.exists():
464
+ return {}
465
+ try:
466
+ partial = json.loads(partial_path.read_text(encoding="utf-8"))
467
+ except (json.JSONDecodeError, OSError):
468
+ return {}
469
+ if not isinstance(partial, dict):
470
+ return {}
471
+ if partial.get("settings") != mimo_video_settings_fingerprint():
472
+ return {}
473
+ if video_path is not None or scenes is not None:
474
+ try:
475
+ if partial.get("provenance") != _mimo_partial_provenance(video_path, scenes):
476
+ return {}
477
+ except (OSError, RuntimeError, TypeError, ValueError):
478
+ return {}
479
+ done = partial.get("chunks")
480
+ if not isinstance(done, dict):
481
+ return {}
482
+ recorded = partial.get("chunks_fingerprint")
483
+ if not recorded or recorded != _mimo_cached_chunks_fingerprint(done):
484
+ return {}
485
+ return done
486
+
487
+
488
+ def _save_mimo_partial(partial_path, done, video_path=None, scenes=None):
489
+ """Persist completed chunk results incrementally so paid chunks survive a mid-loop failure."""
490
+ payload = {
491
+ "settings": mimo_video_settings_fingerprint(),
492
+ "chunks": done,
493
+ "chunks_fingerprint": _mimo_cached_chunks_fingerprint(done),
494
+ }
495
+ if video_path is not None or scenes is not None:
496
+ payload["provenance"] = _mimo_partial_provenance(video_path, scenes)
497
+ partial_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
498
+
499
+
500
+ def _mimo_chunks_match(cached_chunks, expected_chunks):
501
+ if not isinstance(cached_chunks, list) or len(cached_chunks) != len(expected_chunks):
502
+ return False
503
+ try:
504
+ cached_keys = [_mimo_chunk_cache_key(chunk) for chunk in cached_chunks]
505
+ expected_keys = [_mimo_chunk_cache_key(chunk) for chunk in expected_chunks]
506
+ except (KeyError, TypeError, ValueError):
507
+ return False
508
+ return cached_keys == expected_keys
509
+
510
+
511
+ def mimo_video_overview_cache_fresh(overview_path, video_path, scenes):
512
+ """Return True only when the final MiMo overview matches current inputs/settings."""
513
+ overview_path = Path(overview_path) if not hasattr(overview_path, "read_text") else overview_path
514
+ if not overview_path.exists():
515
+ return False
516
+ try:
517
+ overview = json.loads(overview_path.read_text(encoding="utf-8"))
518
+ except (json.JSONDecodeError, OSError):
519
+ return False
520
+ if not isinstance(overview, dict) or overview.get("input") != "scene_chunks":
521
+ return False
522
+ if overview.get("settings") != mimo_video_settings_fingerprint():
523
+ return False
524
+ overview_fingerprint = overview.get("overview_fingerprint")
525
+ if not overview_fingerprint or overview_fingerprint != _mimo_overview_payload_fingerprint(overview):
526
+ return False
527
+ recorded = overview.get("chunks_fingerprint")
528
+ if not recorded:
529
+ return False
530
+ chunks = overview.get("chunks")
531
+ if not isinstance(chunks, list) or not all(
532
+ isinstance(chunk, dict) and _is_mimo_chunk_usable(chunk.get("content"))
533
+ for chunk in chunks
534
+ ):
535
+ return False
536
+ if recorded != _mimo_cached_chunks_fingerprint(chunks):
537
+ return False
538
+ try:
539
+ if overview.get("source_video_fingerprint") != file_fingerprint(video_path):
540
+ return False
541
+ expected_chunks = _mimo_video_chunks(scenes)
542
+ except (OSError, RuntimeError, TypeError, ValueError):
543
+ return False
544
+ return _mimo_chunks_match(chunks, expected_chunks)
545
+
546
+
547
+ def _analyze_mimo_video_chunk(chunk_path, chunk):
548
+ video_url = _video_data_url(chunk_path)
549
+ if not video_url:
550
+ raise RuntimeError(f"MiMo 视频分片 {chunk['chunk_id'] + 1} 超过 data URL 上限")
551
+
552
+ content_parts = [
553
+ {
554
+ "type": "video_url",
555
+ "video_url": {"url": video_url},
556
+ "fps": CONFIG.get("mimo_video_fps", 2.0),
557
+ "media_resolution": CONFIG.get("mimo_media_resolution", "default"),
558
+ },
559
+ {"type": "text", "text": _mimo_chunk_prompt(chunk)},
560
+ ]
561
+ model = _mimo_video_model()
562
+ payload = {
563
+ "model": model,
564
+ "messages": [{"role": "user", "content": content_parts}],
565
+ "max_tokens": 1200,
566
+ }
567
+ resp = mimo_video_api_call(payload)
568
+ try:
569
+ msg = resp["choices"][0]["message"]
570
+ except (KeyError, IndexError, TypeError) as exc:
571
+ raise RuntimeError("MiMo 视频分片理解响应缺少 choices[0].message") from exc
572
+ return {
573
+ "chunk_id": chunk["chunk_id"],
574
+ "scene_id": chunk["scene_id"],
575
+ "start": chunk["start"],
576
+ "end": chunk["end"],
577
+ "model": resp.get("model", model),
578
+ "content": msg.get("content", ""),
579
+ "reasoning_content": msg.get("reasoning_content", ""),
580
+ "usage": resp.get("usage", {}),
581
+ "clip_path": f"mimo_video_chunks/{chunk_path.name}",
582
+ }
583
+
584
+
585
+ _MIMO_REJECTION_MARKERS = (
586
+ "request was rejected", "considered high risk", "high risk",
587
+ "content policy", "cannot process", "无法处理", "内容审核", "违规",
588
+ )
589
+
590
+
591
+ def _is_mimo_chunk_usable(content):
592
+ """A chunk is usable only if MiMo returned real analysis (not empty / a moderation refusal)."""
593
+ text = str(content or "").strip()
594
+ if not text:
595
+ return False
596
+ low = text.lower()
597
+ return not any(marker in low for marker in _MIMO_REJECTION_MARKERS)
598
+
599
+
600
+ def analyze_video_overview(video_path, work_dir, scenes=None):
601
+ """Use MiMo video understanding over local ffmpeg scene chunks."""
602
+ if not CONFIG.get("mimo_video_overview", False):
603
+ return None
604
+ if not CONFIG.get("mimo_video_api_key"):
605
+ log("MiMo 视频概览已启用,但未设置 MIMO_VIDEO_API_KEY/MIMO_API_KEY,跳过")
606
+ return None
607
+
608
+ chunks = _mimo_video_chunks(scenes)
609
+ chunks_dir = work_dir / "mimo_video_chunks"
610
+ chunks_dir.mkdir(exist_ok=True)
611
+
612
+ # 增量缓存:已完成的分片先落盘,避免一段失败时丢弃所有已付费分片
613
+ partial_path = work_dir / "mimo_video_overview.partial.json"
614
+ done = _load_mimo_partial(partial_path, video_path, scenes)
615
+
616
+ log(f"MiMo 视频理解:按 ffmpeg scene 分片分析 {len(chunks)} 段...")
617
+ chunk_results = []
618
+ unusable_chunks = []
619
+ removed_stale_cache = False
620
+ for chunk in chunks:
621
+ cache_key = _mimo_chunk_cache_key(chunk)
622
+ cached = done.get(cache_key)
623
+ if cached is not None:
624
+ if _is_mimo_chunk_usable(cached.get("content")):
625
+ log(
626
+ f" MiMo 分片 {chunk['chunk_id'] + 1}/{len(chunks)}: "
627
+ f"{chunk['start']:.1f}-{chunk['end']:.1f}s(命中增量缓存,跳过)"
628
+ )
629
+ chunk_results.append(cached)
630
+ continue
631
+ done.pop(cache_key, None)
632
+ removed_stale_cache = True
633
+ chunk_path = chunks_dir / (
634
+ f"chunk_{chunk['chunk_id']:03d}_scene_{chunk['scene_id']}_"
635
+ f"{chunk['start']:.2f}-{chunk['end']:.2f}.mp4"
636
+ )
637
+ _extract_video_chunk(video_path, chunk, chunk_path)
638
+ log(
639
+ f" MiMo 分片 {chunk['chunk_id'] + 1}/{len(chunks)}: "
640
+ f"{chunk['start']:.1f}-{chunk['end']:.1f}s"
641
+ )
642
+ chunk_result = _analyze_mimo_video_chunk(chunk_path, chunk)
643
+ if _is_mimo_chunk_usable(chunk_result.get("content")):
644
+ chunk_results.append(chunk_result)
645
+ done[cache_key] = chunk_result
646
+ _save_mimo_partial(partial_path, done, video_path, scenes)
647
+ else:
648
+ unusable_chunks.append(chunk)
649
+ log(f" MiMo 分片 {chunk['chunk_id'] + 1}: 未返回有效内容,保留为待重试")
650
+
651
+ if removed_stale_cache:
652
+ _save_mimo_partial(partial_path, done, video_path, scenes)
653
+
654
+ if not chunk_results:
655
+ log(f"MiMo 视频概览:{len(chunks)} 段均无有效内容(疑似被内容审核拦截),跳过概览")
656
+ try:
657
+ partial_path.unlink()
658
+ except OSError:
659
+ pass
660
+ return None
661
+
662
+ if unusable_chunks:
663
+ # Degrade gracefully instead of aborting the whole understanding: some chunks get
664
+ # moderation-rejected (e.g. a burned-in watermark / violent frames). Write the overview
665
+ # from the usable chunks; the scenes whose chunks were rejected simply fall back to the
666
+ # frame-VLM description downstream. (Aborting here would make overview unsafe to enable
667
+ # by default on any moderated source.)
668
+ sample = ", ".join(str(chunk["chunk_id"] + 1) for chunk in unusable_chunks[:5])
669
+ log(
670
+ f"MiMo 视频概览:{len(unusable_chunks)}/{len(chunks)} 段无有效内容(疑似内容审核),"
671
+ f"以可用分片降级产出,未覆盖场景回退到逐帧描述。样例分片: {sample}"
672
+ )
673
+
674
+ content = "\n\n".join(
675
+ f"### 分片 {item['chunk_id'] + 1} "
676
+ f"(scene {item['scene_id']}, {item['start']:.1f}-{item['end']:.1f}s)\n"
677
+ f"{item['content'].strip()}"
678
+ for item in chunk_results
679
+ )
680
+
681
+ overview = {
682
+ "model": _mimo_video_model(),
683
+ "content": content,
684
+ "chunks": chunk_results,
685
+ "chunk_count": len(chunk_results),
686
+ "fps": CONFIG.get("mimo_video_fps", 2.0),
687
+ "media_resolution": CONFIG.get("mimo_media_resolution", "default"),
688
+ "input": "scene_chunks",
689
+ "partial": bool(unusable_chunks),
690
+ "unusable_chunk_count": len(unusable_chunks),
691
+ "source_video_fingerprint": file_fingerprint(video_path),
692
+ "chunk_max_seconds": CONFIG.get("mimo_video_chunk_max_seconds", 20.0),
693
+ "settings": mimo_video_settings_fingerprint(),
694
+ "chunks_fingerprint": _mimo_cached_chunks_fingerprint(chunk_results),
695
+ }
696
+ overview["overview_fingerprint"] = _mimo_overview_payload_fingerprint(overview)
697
+ overview_path = work_dir / "mimo_video_overview.json"
698
+ overview_path.write_text(json.dumps(overview, ensure_ascii=False, indent=2), encoding="utf-8")
699
+ # 所有分片完成后清理增量缓存,保持 work_dir 仅有规范产物
700
+ try:
701
+ partial_path.unlink()
702
+ except OSError:
703
+ pass
704
+ log(f"MiMo 分片视频概览完成: {overview_path}")
705
+ return overview