@oh-story/dsh 0.1.4 → 0.1.6

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 +2707 -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,470 @@
1
+ """Self-contained config + utilities for this skill (no cross-skill imports).
2
+ Merged from the shared core; reads the same env vars as the rest of the bundle."""
3
+ import json
4
+ import hashlib
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import time
9
+ import urllib.request
10
+ import urllib.error
11
+ from pathlib import Path
12
+ from datetime import datetime, timezone
13
+ from email.utils import parsedate_to_datetime
14
+
15
+
16
+ # ── 配置 ──────────────────────────────────────────────────────────────
17
+
18
+ DEFAULT_MIMO_API_URL = "https://api.xiaomimimo.com/v1"
19
+ DEFAULT_MIMO_TOKEN_PLAN_CLUSTER = "cn"
20
+ MIMO_TOKEN_PLAN_API_URLS = {
21
+ "cn": "https://token-plan-cn.xiaomimimo.com/v1",
22
+ "sgp": "https://token-plan-sgp.xiaomimimo.com/v1",
23
+ "ams": "https://token-plan-ams.xiaomimimo.com/v1",
24
+ }
25
+ DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
26
+ DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
27
+ DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
28
+
29
+
30
+ def normalize_api_url(raw_url):
31
+ """Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
32
+ url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
33
+ if url.endswith("/chat/completions"):
34
+ return url
35
+ return f"{url}/chat/completions"
36
+
37
+
38
+ def is_mimo_token_plan_key(api_key):
39
+ """Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
40
+ return str(api_key or "").strip().startswith("tp-")
41
+
42
+
43
+ def default_mimo_api_url(api_key="", cluster=None):
44
+ """Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
45
+
46
+ MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
47
+ (`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
48
+ not the pay-as-you-go `api.xiaomimimo.com` endpoint.
49
+ """
50
+ if is_mimo_token_plan_key(api_key):
51
+ cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
52
+ cluster_name = str(cluster_name).strip().lower()
53
+ return MIMO_TOKEN_PLAN_API_URLS.get(cluster_name, MIMO_TOKEN_PLAN_API_URLS[DEFAULT_MIMO_TOKEN_PLAN_CLUSTER])
54
+ return DEFAULT_MIMO_API_URL
55
+
56
+
57
+ def env_int(name, default, *, minimum=None):
58
+ """Read an integer env var; ignore malformed values instead of crashing import."""
59
+ raw = os.environ.get(name)
60
+ if raw is None or raw == "":
61
+ return default
62
+ try:
63
+ value = int(raw)
64
+ except (TypeError, ValueError):
65
+ return default
66
+ if minimum is not None:
67
+ value = max(minimum, value)
68
+ return value
69
+
70
+
71
+ def env_bool(name, default=False):
72
+ """Read common boolean env var forms."""
73
+ raw = os.environ.get(name)
74
+ if raw is None or raw == "":
75
+ return default
76
+ return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
77
+
78
+
79
+ def env_float(name, default, *, minimum=None):
80
+ """Read a float env var; ignore malformed values instead of crashing import."""
81
+ raw = os.environ.get(name)
82
+ if raw is None or raw == "":
83
+ return default
84
+ try:
85
+ value = float(raw)
86
+ except (TypeError, ValueError):
87
+ return default
88
+ if minimum is not None:
89
+ value = max(minimum, value)
90
+ return value
91
+
92
+
93
+ # Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
94
+ # (MIMO_VIDEO_API_KEY / MIMO_TTS_API_KEY / MIMO_ASR_API_KEY and their *_API_URL forms)
95
+ # are optional and fall back to MIMO_API_KEY / MIMO_API_URL. Token-Plan keys (tp-*) auto-
96
+ # route to the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
97
+ _mimo_api_key = os.environ.get("MIMO_API_KEY", "")
98
+ _mimo_video_api_key = os.environ.get("MIMO_VIDEO_API_KEY", "") or _mimo_api_key
99
+ _mimo_asr_api_key = os.environ.get("MIMO_ASR_API_KEY", "") or _mimo_api_key
100
+ _raw_api_url = os.environ.get("MIMO_API_URL") or default_mimo_api_url(_mimo_api_key)
101
+ _raw_mimo_video_api_url = (
102
+ os.environ.get("MIMO_VIDEO_API_URL")
103
+ or os.environ.get("MIMO_API_URL")
104
+ or default_mimo_api_url(_mimo_video_api_key)
105
+ )
106
+ _raw_mimo_asr_api_url = (
107
+ os.environ.get("MIMO_ASR_API_URL")
108
+ or os.environ.get("MIMO_API_URL")
109
+ or default_mimo_api_url(_mimo_asr_api_key)
110
+ )
111
+
112
+ CONFIG = {
113
+ "api_url": normalize_api_url(_raw_api_url),
114
+ "api_key": _mimo_api_key,
115
+ "api_key_source": "MIMO_API_KEY",
116
+ "mimo_api_url": normalize_api_url(_raw_api_url),
117
+ "mimo_api_key": _mimo_api_key,
118
+ "mimo_video_api_url": normalize_api_url(_raw_mimo_video_api_url),
119
+ "mimo_video_api_key": _mimo_video_api_key,
120
+ "mimo_asr_api_url": normalize_api_url(_raw_mimo_asr_api_url),
121
+ "mimo_asr_api_key": _mimo_asr_api_key,
122
+ "mimo_asr_api_key_source": "MIMO_ASR_API_KEY" if os.environ.get("MIMO_ASR_API_KEY") else "MIMO_API_KEY",
123
+ "mimo_model": os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
124
+ "mimo_video_model": os.environ.get("MIMO_VIDEO_MODEL") or os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
125
+ "vlm_model": os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
126
+ "mimo_asr_model": os.environ.get("MIMO_ASR_MODEL", DEFAULT_MIMO_ASR_MODEL),
127
+ "mimo_asr_language": os.environ.get("MIMO_ASR_LANGUAGE", "auto"), # auto | zh | en
128
+ "mimo_asr_base64_max_mb": env_float("MIMO_ASR_BASE64_MAX_MB", 10.0, minimum=1.0),
129
+ # ASR 分段窗口秒数。越小 → 长视频的对白时间戳越精细(默认 15s)。旧值 180s 会把 >3min
130
+ # 视频的对白塌缩成一个时间戳,既让 brief 无法定位对白,又触发 detect.py 的粗粒度跳过,
131
+ # 使 overlaps_speech/安静窗口判断失真。代价是更多 ASR 调用;ASR 慢时可调大。
132
+ "asr_segment_seconds": env_float("ASR_SEGMENT_SECONDS", 15.0, minimum=5.0),
133
+ "scene_threshold": 0.1,
134
+ "mimo_media_resolution": os.environ.get("MIMO_MEDIA_RESOLUTION", "default"),
135
+ "mimo_video_overview": env_bool("MIMO_VIDEO_OVERVIEW", False), # opt-in (--mimo-video-overview / =1); when on it becomes the PRIMARY per-scene description, frames stay the anchor/fallback
136
+ "mimo_video_fps": env_float("MIMO_VIDEO_FPS", 3.0, minimum=0.1),
137
+ "mimo_video_chunk_max_seconds": env_float("MIMO_VIDEO_CHUNK_MAX_SECONDS", 20.0, minimum=1.0),
138
+ "mimo_video_chunk_min_seconds": env_float("MIMO_VIDEO_CHUNK_MIN_SECONDS", 1.0, minimum=0.2),
139
+ "mimo_video_chunk_timeout": env_int("MIMO_VIDEO_CHUNK_TIMEOUT", 180, minimum=1),
140
+ "mimo_video_base64_max_mb": env_float("MIMO_VIDEO_BASE64_MAX_MB", 45.0, minimum=1.0),
141
+ # Per-scene frame VLM sampling — scale frames with scene length instead of a hard cap of 6
142
+ "vlm_seconds_per_frame": env_float("VLM_SECONDS_PER_FRAME", 4.0, minimum=0.5),
143
+ "vlm_max_frames": env_int("VLM_MAX_FRAMES", 16, minimum=3),
144
+ "vlm_max_tokens": env_int("VLM_MAX_TOKENS", 1500, minimum=200),
145
+ "mimo_video_prompt": os.environ.get(
146
+ "MIMO_VIDEO_PROMPT",
147
+ "请用中文分析这个视频分片的主要人物、场景变化、关键动作、情绪走向和剧情冲突,"
148
+ "重点提取适合写短视频解说的故事线索。不要泛泛复述画面,要标出对后续写稿有用的信息。",
149
+ ),
150
+ "mimo_disable_thinking": env_bool("MIMO_DISABLE_THINKING", True),
151
+ "fps": 0, # 0 = 自动(≤60s→2fps, ≤5min→1.5fps, >5min→1fps)
152
+ # Storyboard contact sheets are advisory and generated locally where frames/fps are owned.
153
+ # Keep these keys only in the stage that consumes them; other stages do not read storyboard_*.
154
+ "storyboard": env_bool("STORYBOARD", True), # generate source/edited storyboard contact sheets
155
+ "storyboard_max_tiles": env_int("STORYBOARD_MAX_TILES", 30, minimum=1), # cap tiles per sheet for legibility
156
+ "storyboard_columns": env_int("STORYBOARD_COLUMNS", 6, minimum=1), # tile grid columns
157
+ "storyboard_rows_per_page": env_int("STORYBOARD_ROWS_PER_PAGE", 5, minimum=1), # tile grid rows before spilling to the next sheet
158
+ "storyboard_long_scene_seconds": env_float("STORYBOARD_LONG_SCENE_SECONDS", 6.0, minimum=0.1), # scenes at/above this also sample +1/3 & +2/3
159
+ # TTS 语速(字符/秒)。实测 mimo-tts 冰糖音色中位 ~3.9 字/秒,可用 SPEECH_RATE 覆盖
160
+ # 生成解说时使用 speech_rate * safety_margin 作为约束
161
+ "speech_rate": env_float("SPEECH_RATE", 3.9, minimum=0.5), # 旧值 3.5 系统性偏低 ~10-17%
162
+ "speech_safety_margin": env_float("SPEECH_SAFETY_MARGIN", 0.85, minimum=0.1), # 保守系数:TTS 实际语速有 ±20% 波动
163
+ # Block-coverage lint thresholds — promoted from inline .get() literals to real CONFIG keys (tunable; defaults unchanged)
164
+ "narration_coverage_target": 0.7, # rough first-draft/diagnostic fallback; content-led audio decisions may differ (not a quota)
165
+ "narration_coverage_max": 0.85, # above this coverage → no_original_blocks (narration is wall-to-wall)
166
+ "narration_coverage_min": 0.5, # below this coverage → under_narrated
167
+ "narration_block_seconds": 9.0, # block cadence used to derive target block count
168
+ "original_block_min_seconds": 2.5, # a deliberate original-audio gap must be at least this long
169
+ "narration_block_min_chars": 16, # below this avg block size → fragmented_beats
170
+ "breath_ms": 250, # 段间呼吸空间(ms);block recap 块内连贯、块间留原声呼吸
171
+ "narration_speed": env_float("NARRATION_SPEED", 1.15, minimum=0.5), # 解说整体提速(atempo),默认回到可懂区间;长片可设 1.0
172
+ "narration_tail_pad_seconds": 0.1, # 解说尾部最少留白;短 slot 会自动压低 delay 避免截断
173
+ "quiet_overlap_min_ratio": 0.8, # 解说段至少多少比例落在安静窗口内才标记为非对白重叠
174
+ "visual_beat_max_seconds": 18.0, # 单段解说超过该时长且跨多个帧锚点时给 lint 提醒
175
+ "visual_beat_max_facts": 3, # 单段解说最多建议覆盖的 frame_facts 锚点数量
176
+ "asr_chunk_min_chars": env_int("ASR_CHUNK_MIN_CHARS", 500, minimum=1), # brief 中 ASR 写作分块最小字数/词数
177
+ "asr_chunk_max_chars": env_int("ASR_CHUNK_MAX_CHARS", 800, minimum=1), # brief 中 ASR 写作分块最大字数/词数
178
+ "silence_noise_threshold": "-25dB", # ffmpeg silencedetect 噪声阈值
179
+ # Short acoustic pauses are not quiet windows; they are sentence-boundary candidates.
180
+ # A looser threshold plus ASR-punctuation alignment lets the writer enter after a full
181
+ # source sentence instead of ducking it halfway through.
182
+ "source_boundary_noise_threshold": os.environ.get("SOURCE_BOUNDARY_NOISE_THRESHOLD", "-18dB"),
183
+ "source_boundary_min_pause": env_float("SOURCE_BOUNDARY_MIN_PAUSE", 0.12, minimum=0.05),
184
+ "source_boundary_max_alignment_error": env_float(
185
+ "SOURCE_BOUNDARY_MAX_ALIGNMENT_ERROR", 2.1, minimum=0.2
186
+ ),
187
+ "silence_min_duration": 0.3, # 静音最短持续秒数
188
+ "quiet_window_min": 1.0, # 可放解说的安静窗口最短秒数
189
+ "silence_merge_gap": 0.5, # 相邻静音段间隔<此值时合并
190
+ "scene_merge_min": 4.0, # 场景合并最短时长,<此值的场景合并到相邻场景
191
+ "scene_junk_filter": env_bool("SCENE_JUNK_FILTER", True), # 过滤连续黑/白帧无效过渡场景
192
+ "scene_junk_workers": env_int("SCENE_JUNK_WORKERS", 8, minimum=1), # 黑/白帧探测并行度(每个探点一次 ffmpeg)
193
+ "scene_junk_dark_luma": env_float("SCENE_JUNK_DARK_LUMA", 8.0, minimum=0.0),
194
+ "scene_junk_bright_luma": env_float("SCENE_JUNK_BRIGHT_LUMA", 245.0, minimum=0.0),
195
+ "scene_junk_pixel_ratio": env_float("SCENE_JUNK_PIXEL_RATIO", 0.995, minimum=0.0),
196
+ "context_info": "", # 额外上下文(节目名、角色名等)
197
+ "vlm_workers": env_int("VLM_WORKERS", 8, minimum=1), # VLM 并行分析线程数
198
+ "edit_mode": os.environ.get("EDIT_MODE", "full"), # full | cut
199
+ "target_duration": os.environ.get("TARGET_DURATION", ""), # cut 模式目标成片时长,如 10m
200
+ }
201
+
202
+ SCRIPT_DIR = Path(__file__).parent
203
+ PROMPTS_DIR = SCRIPT_DIR.parent / "references"
204
+
205
+ def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
206
+ """Return the canonical tempo budget shared by voiceover and assemble."""
207
+ cfg = config or CONFIG
208
+ global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
209
+ rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
210
+ cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
211
+ hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
212
+ legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
213
+ segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
214
+ return {
215
+ "global_narration_speed": global_speed,
216
+ "tts_rate_factor": rate_factor,
217
+ "cumulative_tempo_max": cumulative_max,
218
+ "cumulative_tempo_hard_max": hard_max,
219
+ "segment_tempo_max": segment_tempo_max,
220
+ "max_raw_duration_factor": global_speed * segment_tempo_max,
221
+ }
222
+
223
+ def log(msg):
224
+ print(f"[video-recap] {msg}", flush=True)
225
+
226
+ def run_cmd(cmd, **kwargs):
227
+ """运行命令,返回 CompletedProcess"""
228
+ if isinstance(cmd, list):
229
+ display_parts = []
230
+ for part in cmd:
231
+ text = str(part)
232
+ display_parts.append(text if len(text) <= 240 else text[:237] + "...")
233
+ display = " ".join(display_parts)
234
+ else:
235
+ display = str(cmd)
236
+ if len(display) > 2000:
237
+ display = display[:1997] + "..."
238
+ log(f"运行: {display}")
239
+ return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
240
+
241
+ def get_video_duration(video_path):
242
+ """获取视频时长(秒)"""
243
+ cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
244
+ "-of", "csv=p=0", str(video_path)]
245
+ result = run_cmd(cmd)
246
+ if result.returncode != 0:
247
+ return 0.0
248
+ try:
249
+ return float(result.stdout.strip())
250
+ except (TypeError, ValueError):
251
+ return 0.0
252
+
253
+ def stable_json_dumps(value):
254
+ """Serialize values deterministically for non-secret cache fingerprints."""
255
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
256
+
257
+ def stable_hash(value):
258
+ """Return an md5 digest for deterministic JSON-serializable values."""
259
+ return hashlib.md5(stable_json_dumps(value).encode("utf-8")).hexdigest()
260
+
261
+ _FILE_FINGERPRINT_MEMO = {}
262
+
263
+
264
+ def _file_identity(path):
265
+ """(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
266
+ st = os.stat(os.fspath(path))
267
+ return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
268
+
269
+
270
+ def file_fingerprint(path, chunk_size=1024 * 1024):
271
+ """Return a full-content fingerprint for cache-correct identity checks.
272
+
273
+ The digest covers CONTENT only — never the path or mtime — so a copied video or
274
+ artifact is still recognised as the same asset, while any byte change invalidates
275
+ the cache even if timestamps, size, head, or tail bytes are misleading.
276
+
277
+ Identity metadata is used ONLY to memoize within a single process. One understanding
278
+ run fingerprints the same source video 8-10 times and the whole extracted frame set
279
+ 2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
280
+ any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
281
+ re-hashed, so the memo can never serve a stale digest.
282
+ """
283
+ key = _file_identity(path)
284
+ memoized = _FILE_FINGERPRINT_MEMO.get(key)
285
+ if memoized is not None:
286
+ return memoized
287
+ h = hashlib.sha256()
288
+ with open(os.fspath(path), "rb") as f:
289
+ for chunk in iter(lambda: f.read(chunk_size), b""):
290
+ h.update(chunk)
291
+ digest = h.hexdigest()
292
+ _FILE_FINGERPRINT_MEMO[key] = digest
293
+ return digest
294
+ def video_fingerprint(video_path):
295
+ """Full video content fingerprint used as the root pipeline asset print."""
296
+ return file_fingerprint(video_path)
297
+
298
+ def step_cache_key(video_path, step_name, params_fingerprint=""):
299
+ """Build a cache key from video content, step name and step parameters."""
300
+ params_digest = params_fingerprint
301
+ if not isinstance(params_digest, str):
302
+ params_digest = stable_hash(params_digest)
303
+ payload = f"{video_fingerprint(video_path)}_{step_name}_{params_digest}"
304
+ return hashlib.md5(payload.encode("utf-8")).hexdigest()
305
+
306
+ def _retry_after_seconds(value, fallback):
307
+ """Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
308
+ if not value:
309
+ return fallback
310
+ try:
311
+ return max(fallback, max(0, int(value)))
312
+ except (TypeError, ValueError):
313
+ pass
314
+ try:
315
+ retry_at = parsedate_to_datetime(value)
316
+ if retry_at.tzinfo is None:
317
+ retry_at = retry_at.replace(tzinfo=timezone.utc)
318
+ return max(fallback, max(0, int((retry_at - datetime.now(timezone.utc)).total_seconds())))
319
+ except (TypeError, ValueError, IndexError, OverflowError):
320
+ return fallback
321
+
322
+
323
+ _ERROR_DATA_URL_RE = re.compile(
324
+ r"data:(?:audio|video|image)/[^;,\s\"'<>]+;base64,[A-Za-z0-9+/=]+",
325
+ re.IGNORECASE,
326
+ )
327
+ _ERROR_KEY_RE = re.compile(r"\b(?:tp|sk)-[A-Za-z0-9_-]{8,}\b")
328
+
329
+
330
+ def _sanitize_api_error(value, limit=500):
331
+ """Bound transport diagnostics without echoing request media or credentials."""
332
+ text = _ERROR_DATA_URL_RE.sub("<redacted-data-url>", str(value or ""))
333
+ text = _ERROR_KEY_RE.sub("<redacted-key>", text)
334
+ return text[:limit]
335
+
336
+ def _api_headers(api_provider=None, api_url=None, api_key=None):
337
+ """Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
338
+ del api_provider, api_url # MiMo is the only provider; signature kept for call sites
339
+ key = CONFIG.get("api_key", "") if api_key is None else api_key
340
+ return {
341
+ "Content-Type": "application/json",
342
+ "User-Agent": "video-recap/1.0",
343
+ "api-key": key,
344
+ }
345
+
346
+ def _prepare_api_payload(payload, api_provider=None, api_url=None):
347
+ """Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
348
+ del api_provider, api_url
349
+ normalized = dict(payload)
350
+ if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
351
+ normalized["max_completion_tokens"] = normalized.pop("max_tokens")
352
+ model = str(normalized.get("model") or "")
353
+ if (
354
+ CONFIG.get("mimo_disable_thinking", True)
355
+ and not model.endswith(("-tts", "-asr"))
356
+ and "thinking" not in normalized
357
+ ):
358
+ # MiMo V2.5 may spend small max_completion_tokens budgets on reasoning_content.
359
+ # The recap pipeline needs visible text, so disable thinking unless set explicitly.
360
+ normalized["thinking"] = {"type": "disabled"}
361
+ return normalized
362
+
363
+ def _mimo_endpoint(kind):
364
+ """Return per-capability MiMo endpoint settings (video understanding / TTS / ASR)."""
365
+ by_kind = {
366
+ "video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_api_key_source"),
367
+ "tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_api_key_source"),
368
+ "asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_api_key_source"),
369
+ }
370
+ if kind not in by_kind:
371
+ raise ValueError(f"Unsupported MiMo endpoint kind: {kind}")
372
+ url_key, key_key, src_key = by_kind[kind]
373
+ return {
374
+ "api_url": CONFIG.get(url_key) or CONFIG.get("mimo_api_url"),
375
+ "api_key": CONFIG.get(key_key) or CONFIG.get("mimo_api_key"),
376
+ "api_key_source": CONFIG.get(src_key, "MIMO_API_KEY"),
377
+ }
378
+
379
+ def _call_mimo_endpoint(kind, payload, max_retries=10):
380
+ settings = _mimo_endpoint(kind)
381
+ return api_call(
382
+ payload,
383
+ max_retries=max_retries,
384
+ api_provider="mimo",
385
+ api_url=settings["api_url"],
386
+ api_key=settings["api_key"],
387
+ api_key_source=settings["api_key_source"],
388
+ )
389
+
390
+ def mimo_video_api_call(payload, max_retries=10):
391
+ """Call the MiMo video-understanding endpoint."""
392
+ return _call_mimo_endpoint("video", payload, max_retries=max_retries)
393
+
394
+ def mimo_asr_api_call(payload, max_retries=10):
395
+ """Call the MiMo speech-recognition (ASR) endpoint."""
396
+ return _call_mimo_endpoint("asr", payload, max_retries=max_retries)
397
+
398
+ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_key_source=None):
399
+ """调用 OpenAI-compatible API,带重试。
400
+
401
+ 长视频理解会发出数百次 VLM/ASR 调用,集群的 429 限流是常态而非错误,所以重试更耐心
402
+ (更多次数 + 退避封顶 60s + 遵从 Retry-After),避免一次瞬时限流就中止整段理解。
403
+ 集群的配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s,给窗口时间复位。
404
+ """
405
+ endpoint = normalize_api_url(api_url if api_url is not None else CONFIG["api_url"])
406
+ headers = _api_headers(api_provider=api_provider, api_url=endpoint, api_key=api_key)
407
+ data = json.dumps(_prepare_api_payload(payload, api_provider=api_provider, api_url=endpoint)).encode("utf-8")
408
+
409
+ for attempt in range(max_retries):
410
+ try:
411
+ req = urllib.request.Request(endpoint, data=data, headers=headers)
412
+ with urllib.request.urlopen(req, timeout=300) as resp:
413
+ result = json.loads(resp.read().decode("utf-8"))
414
+ return result
415
+ except urllib.error.HTTPError as e:
416
+ body = _sanitize_api_error(e.read().decode("utf-8", errors="replace"))
417
+ wait = min(2 ** attempt, 60)
418
+ if e.code == 429:
419
+ retry_after = e.headers.get("Retry-After")
420
+ wait = _retry_after_seconds(retry_after, max(wait, 10))
421
+ log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
422
+ elif e.code == 401:
423
+ key_name = api_key_source or CONFIG.get("api_key_source", "MIMO_API_KEY")
424
+ raise RuntimeError(f"API 认证失败 (401)。请检查 {key_name} 和 API URL 是否匹配。")
425
+ elif e.code == 403:
426
+ hint = "API 访问被拒绝 (403)。"
427
+ if "1010" in body or "cloudflare" in body.lower():
428
+ hint += "IP 被 Cloudflare 限流,请等待几分钟后重试。"
429
+ raise RuntimeError(hint)
430
+ hint += "请检查 API key 权限和 API URL 设置。"
431
+ raise RuntimeError(hint)
432
+ elif e.code == 405:
433
+ raise RuntimeError("API 端点不可用 (405),可能被 WAF 拦截。请检查 MIMO_API_URL 或稍后重试。")
434
+ elif e.code == 503:
435
+ log(f"API 服务暂不可用 (503),等待 {wait}s (尝试 {attempt+1}/{max_retries})")
436
+ elif e.code == 524:
437
+ # Cloudflare 超时:服务端处理超时,需要更长退避
438
+ wait = max(wait, 4 * (attempt + 1))
439
+ log(f"API 超时 (524),等待 {wait}s (尝试 {attempt+1}/{max_retries})")
440
+ else:
441
+ log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): HTTP {e.code} — {body}")
442
+ if attempt < max_retries - 1:
443
+ time.sleep(wait)
444
+ else:
445
+ raise RuntimeError(f"API 调用失败 {max_retries} 次: HTTP {e.code} — {body}")
446
+ except Exception as e: # noqa: BLE001 - transport/decode faults are all retryable here
447
+ # (Deliberately broad, but no longer written as `(URLError, Exception)`, which
448
+ # read as a tuple while `Exception` already subsumed the first member.)
449
+ wait = min(2 ** attempt, 60)
450
+ safe_error = _sanitize_api_error(e)
451
+ log(f"API 调用失败 (尝试 {attempt+1}/{max_retries}): {safe_error}")
452
+ if attempt < max_retries - 1:
453
+ log(f"等待 {wait}s 后重试...")
454
+ time.sleep(wait)
455
+ else:
456
+ raise RuntimeError(f"API 调用失败 {max_retries} 次: {safe_error}")
457
+ # Unreachable for max_retries >= 1; guards against a silent `None` return (and the
458
+ # TypeError it would cause at the caller's resp["choices"]) if a caller passes 0.
459
+ raise ValueError(f"max_retries must be >= 1, got {max_retries}")
460
+
461
+ def load_prompt(name):
462
+ """加载 prompt 模板"""
463
+ path = PROMPTS_DIR / "prompt-templates.md"
464
+ if not path.exists():
465
+ return None
466
+ content = path.read_text(encoding="utf-8")
467
+ # 用 ### NAME 和 ### 分隔提取对应 prompt
468
+ pattern = rf"### {name}\s*\n(.*?)(?=\n### |\Z)"
469
+ m = re.search(pattern, content, re.DOTALL)
470
+ return m.group(1).strip() if m else None