@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,394 @@
1
+ """Safe writer and portable media bundler for JianYing draft folders."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ import time
9
+ import uuid
10
+ import zipfile
11
+
12
+ from jianying_templates import template
13
+
14
+
15
+ DRAFT_PATH_PLACEHOLDER = "##_draftpath_placeholder_0E685133-18CE-45ED-8CB8-2904A212EC80_##"
16
+
17
+
18
+ def validate_draft_name(draft_name):
19
+ """Reject draft names that could escape or alias the requested parent dir."""
20
+ if not isinstance(draft_name, str):
21
+ raise TypeError("draft_name must be a string")
22
+ if not draft_name or not draft_name.strip():
23
+ raise ValueError("draft_name must not be empty")
24
+ if os.path.isabs(draft_name):
25
+ raise ValueError("draft_name must be a plain folder name, not an absolute path")
26
+ if "/" in draft_name or "\\" in draft_name:
27
+ raise ValueError("draft_name must not contain path separators")
28
+ if draft_name in {".", ".."}:
29
+ raise ValueError("draft_name must not be '.' or '..'")
30
+
31
+
32
+ def _resource_kind(material, materials_key):
33
+ if materials_key == "audios":
34
+ return "audio", "music"
35
+ if material.get("type") == "photo":
36
+ return "image", "photo"
37
+ return "video", "video"
38
+
39
+
40
+ RESOURCE_DIRECTORY_BY_MATERIALS_KEY = {
41
+ "chromas": "effect",
42
+ "common_mask": "mask",
43
+ "effects": "effect",
44
+ "masks": "mask",
45
+ "stickers": "sticker",
46
+ "texts": "text",
47
+ "text_templates": "text_template",
48
+ "transitions": "transition",
49
+ "video_effects": "effect",
50
+ }
51
+
52
+
53
+ def _unused_name(directory, basename, used):
54
+ """Return a collision-free filename within one resource directory."""
55
+ name = basename
56
+ stem, ext = os.path.splitext(basename)
57
+ suffix = 1
58
+ while name in used or os.path.exists(os.path.join(directory, name)):
59
+ name = f"{stem}_{suffix}{ext}"
60
+ suffix += 1
61
+ used.add(name)
62
+ return name
63
+
64
+
65
+ def _md5(path):
66
+ digest = hashlib.md5(usedforsecurity=False)
67
+ with open(path, "rb") as source:
68
+ for chunk in iter(lambda: source.read(1024 * 1024), b""):
69
+ digest.update(chunk)
70
+ return digest.hexdigest()
71
+
72
+
73
+ def _meta_value(material, relative_path, metetype, copied_path, timestamp_ms):
74
+ duration = int(material.get("duration") or 0)
75
+ timestamp_s = timestamp_ms // 1000
76
+ value = template("meta_material")
77
+ value.update({
78
+ "duration": duration,
79
+ "height": int(material.get("height") or 0),
80
+ # duo-video indexes imported local resources independently from the
81
+ # draft-content material IDs.
82
+ "id": str(uuid.uuid4()).upper(),
83
+ "md5": _md5(copied_path),
84
+ "metetype": metetype,
85
+ "type": 0,
86
+ "width": int(material.get("width") or 0),
87
+ "create_time": timestamp_s,
88
+ "extra_info": os.path.basename(relative_path),
89
+ "file_Path": f"./{relative_path}",
90
+ "import_time": timestamp_s,
91
+ "import_time_ms": timestamp_ms,
92
+ "item_source": 1,
93
+ "roughcut_time_range": {"duration": duration, "start": 0},
94
+ "sub_time_range": {"duration": -1, "start": -1},
95
+ })
96
+ return value
97
+
98
+
99
+ def _material_sets(content):
100
+ """Yield root and nested compound-draft material dictionaries."""
101
+ materials = content.get("materials")
102
+ if not isinstance(materials, dict):
103
+ return
104
+ yield materials
105
+ for draft in materials.get("drafts", []):
106
+ nested = draft.get("draft") if isinstance(draft, dict) else None
107
+ if isinstance(nested, dict):
108
+ yield from _material_sets(nested)
109
+
110
+
111
+ def _replace_value(value, old, new):
112
+ if isinstance(value, dict):
113
+ for key, item in value.items():
114
+ value[key] = _replace_value(item, old, new)
115
+ elif isinstance(value, list):
116
+ for index, item in enumerate(value):
117
+ value[index] = _replace_value(item, old, new)
118
+ elif isinstance(value, str):
119
+ if value == old:
120
+ return new
121
+ stripped = value.lstrip()
122
+ if stripped.startswith(("{", "[")):
123
+ try:
124
+ parsed = json.loads(value)
125
+ except json.JSONDecodeError:
126
+ return value
127
+ replaced = _replace_value(parsed, old, new)
128
+ return json.dumps(replaced, ensure_ascii=False, separators=(",", ":"))
129
+ return value
130
+
131
+
132
+ def _safe_resource_target(target_path):
133
+ normalized = os.path.normpath(str(target_path).replace("\\", "/"))
134
+ if normalized in {"", "."} or os.path.isabs(normalized):
135
+ raise ValueError(f"invalid JianYing resource target_path: {target_path}")
136
+ if normalized == ".." or normalized.startswith("../"):
137
+ raise ValueError(f"JianYing resource target_path escapes package: {target_path}")
138
+ return normalized
139
+
140
+
141
+ def _extract_zip(source, destination):
142
+ os.makedirs(destination, exist_ok=False)
143
+ destination_real = os.path.realpath(destination)
144
+ with zipfile.ZipFile(source) as archive:
145
+ for member in archive.infolist():
146
+ member_path = os.path.realpath(os.path.join(destination, member.filename))
147
+ if os.path.commonpath((destination_real, member_path)) != destination_real:
148
+ raise ValueError(f"unsafe path in JianYing resource archive: {member.filename}")
149
+ archive.extractall(destination)
150
+
151
+
152
+ def _copy_resource(source, resource_kind, resources_root, used, target_path=None):
153
+ resource_dir = os.path.join(resources_root, resource_kind)
154
+ os.makedirs(resource_dir, exist_ok=True)
155
+ is_zip = os.path.isfile(source) and zipfile.is_zipfile(source)
156
+ if target_path is None:
157
+ basename = os.path.basename(source.rstrip(os.sep))
158
+ if is_zip:
159
+ basename = os.path.splitext(basename)[0]
160
+ relative_target = _unused_name(resource_dir, basename, used[resource_kind])
161
+ else:
162
+ relative_target = _safe_resource_target(target_path)
163
+ copied_path = os.path.join(resource_dir, relative_target)
164
+ copied_real = os.path.realpath(copied_path)
165
+ if os.path.commonpath((os.path.realpath(resource_dir), copied_real)) != os.path.realpath(resource_dir):
166
+ raise ValueError(f"JianYing resource target escapes package: {target_path}")
167
+ os.makedirs(os.path.dirname(copied_path), exist_ok=True)
168
+ if os.path.exists(copied_path):
169
+ raise FileExistsError(f"duplicate JianYing resource target: {relative_target}")
170
+ if is_zip:
171
+ _extract_zip(source, copied_path)
172
+ elif os.path.isdir(source):
173
+ shutil.copytree(source, copied_path)
174
+ else:
175
+ shutil.copy2(source, copied_path)
176
+ relative_path = f"Resources/local/{resource_kind}/{relative_target.replace(os.sep, '/')}"
177
+ return copied_path, relative_path, f"{DRAFT_PATH_PLACEHOLDER}/{relative_path}"
178
+
179
+
180
+ def _is_packaged_path(value):
181
+ return str(value).startswith((DRAFT_PATH_PLACEHOLDER, "Resources/", "./Resources/"))
182
+
183
+
184
+ def _descriptor(raw, default_kind, *, required):
185
+ if isinstance(raw, str):
186
+ source = raw
187
+ resource_kind = default_kind
188
+ target_path = None
189
+ elif isinstance(raw, dict):
190
+ source = raw.get("source_path") or raw.get("source")
191
+ resource_kind = raw.get("resource_kind") or raw.get("kind") or default_kind
192
+ target_path = raw.get("target_path")
193
+ else:
194
+ raise ValueError("JianYing resources entries must be paths or objects")
195
+ if not isinstance(source, str) or not source:
196
+ raise ValueError("JianYing resource source_path must be a non-empty string")
197
+ if not isinstance(resource_kind, str) or resource_kind not in {
198
+ "audio", "effect", "fonts", "image", "lut", "mask", "sticker",
199
+ "text", "text_template", "transition", "video",
200
+ }:
201
+ raise ValueError(f"invalid JianYing resource kind: {resource_kind}")
202
+ suffix = os.path.splitext(source)[1].lower()
203
+ if suffix == ".cube":
204
+ resource_kind = "lut"
205
+ elif suffix in {".ttf", ".otf"}:
206
+ resource_kind = "fonts"
207
+ return {
208
+ "source_path": source,
209
+ "resource_kind": resource_kind,
210
+ "target_path": target_path,
211
+ "required": required,
212
+ }
213
+
214
+
215
+ def _material_resource_descriptors(material, default_kind):
216
+ descriptors = [
217
+ _descriptor(raw, default_kind, required=True)
218
+ for raw in material.get("_bundle_resources", [])
219
+ ]
220
+ path = material.get("path")
221
+ if isinstance(path, str) and path and not _is_packaged_path(path):
222
+ if not any(item["source_path"] == path for item in descriptors):
223
+ descriptors.append(_descriptor(path, default_kind, required=False))
224
+ return descriptors
225
+
226
+
227
+ def bundle_media(content, meta, draft_dir):
228
+ """Copy media into duo-video's Resources/local contract and index it.
229
+
230
+ Materials that reference the same source file and resource kind share one
231
+ copied file and one meta entry. Missing sources remain untouched and are
232
+ reported to the caller so a non-portable reference is never disguised as a
233
+ successfully bundled one.
234
+ """
235
+ resources_root = os.path.join(draft_dir, "Resources", "local")
236
+ copied = {}
237
+ resource_kinds = {
238
+ "audio", "effect", "fonts", "image", "lut", "mask", "sticker",
239
+ "text", "text_template", "transition", "video",
240
+ }
241
+ used = {kind: set() for kind in resource_kinds}
242
+ meta_values = []
243
+ notes = []
244
+ timestamp_ms = int(time.time() * 1000)
245
+
246
+ material_sets = list(_material_sets(content))
247
+ for materials in material_sets:
248
+ for materials_key in ("videos", "audios"):
249
+ for material in materials.get(materials_key, []):
250
+ src = material.get("path")
251
+ if not src:
252
+ continue
253
+ resource_kind, metetype = _resource_kind(material, materials_key)
254
+ source_key = (resource_kind, os.path.realpath(src))
255
+ existing = copied.get(source_key)
256
+ if existing is not None:
257
+ material["path"] = existing["draft_path"]
258
+ continue
259
+ if not os.path.isfile(src):
260
+ if not _is_packaged_path(src):
261
+ notes.append(f"素材缺失,未打包: {src}")
262
+ continue
263
+
264
+ copied_path, relative_path, draft_path = _copy_resource(
265
+ src, resource_kind, resources_root, used
266
+ )
267
+ material["path"] = draft_path
268
+ copied[source_key] = {"draft_path": draft_path}
269
+ meta_values.append(
270
+ _meta_value(material, relative_path, metetype, copied_path, timestamp_ms)
271
+ )
272
+
273
+ for materials_key, resource_kind in RESOURCE_DIRECTORY_BY_MATERIALS_KEY.items():
274
+ for material in materials.get(materials_key, []):
275
+ descriptors = _material_resource_descriptors(
276
+ material, resource_kind
277
+ )
278
+ seen_descriptors = set()
279
+ for descriptor in descriptors:
280
+ src = descriptor["source_path"]
281
+ if _is_packaged_path(src):
282
+ continue
283
+ descriptor_key = (
284
+ descriptor["resource_kind"],
285
+ os.path.realpath(src),
286
+ descriptor["target_path"],
287
+ )
288
+ if descriptor_key in seen_descriptors:
289
+ continue
290
+ seen_descriptors.add(descriptor_key)
291
+ if not os.path.exists(src):
292
+ message = f"声明的剪映资源缺失: {src}"
293
+ if descriptor["required"]:
294
+ raise ValueError(message)
295
+ notes.append(message)
296
+ continue
297
+ kind = descriptor["resource_kind"]
298
+ source_key = (kind, os.path.realpath(src), descriptor["target_path"])
299
+ existing = copied.get(source_key)
300
+ if existing is None:
301
+ _copied_path, _relative_path, draft_path = _copy_resource(
302
+ src,
303
+ kind,
304
+ resources_root,
305
+ used,
306
+ target_path=descriptor["target_path"],
307
+ )
308
+ copied[source_key] = {"draft_path": draft_path}
309
+ else:
310
+ draft_path = existing["draft_path"]
311
+ _replace_value(material, src, draft_path)
312
+
313
+ material_group = next(group for group in meta["draft_materials"] if group["type"] == 0)
314
+ material_group["value"] = meta_values
315
+ meta["draft_timeline_materials_size_"] = sum(
316
+ os.path.getsize(os.path.join(draft_dir, value["file_Path"][2:]))
317
+ for value in meta_values
318
+ )
319
+ return notes
320
+
321
+
322
+ def strip_internal_resource_fields(content):
323
+ for materials in _material_sets(content):
324
+ for entries in materials.values():
325
+ if not isinstance(entries, list):
326
+ continue
327
+ for material in entries:
328
+ if isinstance(material, dict):
329
+ material.pop("_bundle_resources", None)
330
+
331
+
332
+ def draft_dir_has_user_content(draft_dir):
333
+ """Return True when writing here could overwrite an existing draft/material."""
334
+ if not os.path.exists(draft_dir):
335
+ return False
336
+ try:
337
+ return any(os.scandir(draft_dir))
338
+ except OSError:
339
+ return True
340
+
341
+
342
+ def collision_safe_draft_dir(out_dir, draft_name):
343
+ """Pick a fresh draft folder instead of overwriting an existing non-empty one."""
344
+ validate_draft_name(draft_name)
345
+ base = os.path.join(out_dir, draft_name)
346
+ if not draft_dir_has_user_content(base):
347
+ return base, draft_name
348
+ idx = 2
349
+ while True:
350
+ candidate_name = f"{draft_name}_{idx}"
351
+ candidate = os.path.join(out_dir, candidate_name)
352
+ if not draft_dir_has_user_content(candidate):
353
+ return candidate, candidate_name
354
+ idx += 1
355
+
356
+
357
+ def write_draft(content, meta, notes, out_dir, draft_name, bundle_media_enabled=False):
358
+ """Atomically write the three JianYing draft JSON files and optional bundle."""
359
+ validate_draft_name(draft_name)
360
+ out_dir = os.path.abspath(out_dir)
361
+ os.makedirs(out_dir, exist_ok=True)
362
+ draft_dir, actual_name = collision_safe_draft_dir(out_dir, draft_name)
363
+ notes = list(notes)
364
+ if actual_name != draft_name:
365
+ notes.append(f"草稿目录已存在,改写为 {actual_name} 以避免覆盖")
366
+
367
+ tmp_parent = tempfile.mkdtemp(prefix=f".{actual_name}.", dir=out_dir)
368
+ tmp_dir = os.path.join(tmp_parent, actual_name)
369
+ try:
370
+ os.makedirs(tmp_dir, exist_ok=False)
371
+ if bundle_media_enabled:
372
+ notes.extend(bundle_media(content, meta, tmp_dir))
373
+ strip_internal_resource_fields(content)
374
+ timestamp_ms = int(time.time() * 1000)
375
+ meta["draft_name"] = actual_name
376
+ meta["draft_fold_path"] = draft_dir
377
+ meta["tm_draft_create"] = timestamp_ms
378
+ meta["tm_draft_modified"] = timestamp_ms
379
+ content["name"] = actual_name
380
+ for fname in ("draft_content.json", "draft_info.json"):
381
+ with open(os.path.join(tmp_dir, fname), "w", encoding="utf-8") as f:
382
+ json.dump(content, f, ensure_ascii=False, indent=2)
383
+ with open(os.path.join(tmp_dir, "draft_meta_info.json"), "w", encoding="utf-8") as f:
384
+ json.dump(meta, f, ensure_ascii=False, indent=2)
385
+ if os.path.isdir(draft_dir) and not draft_dir_has_user_content(draft_dir):
386
+ os.rmdir(draft_dir)
387
+ os.replace(tmp_dir, draft_dir)
388
+ except Exception:
389
+ shutil.rmtree(tmp_parent, ignore_errors=True)
390
+ raise
391
+ finally:
392
+ if os.path.exists(tmp_parent):
393
+ shutil.rmtree(tmp_parent, ignore_errors=True)
394
+ return draft_dir, notes
@@ -0,0 +1,239 @@
1
+ """Self-contained config + utilities for this skill (no cross-skill imports)."""
2
+ import os
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+
7
+ # ── 配置 ──────────────────────────────────────────────────────────────
8
+ _EXISTING_CONFIG_REF = globals().get("CONFIG")
9
+
10
+ DEFAULT_MIMO_API_URL = "https://api.xiaomimimo.com/v1"
11
+ DEFAULT_MIMO_TOKEN_PLAN_CLUSTER = "cn"
12
+ MIMO_TOKEN_PLAN_API_URLS = {
13
+ "cn": "https://token-plan-cn.xiaomimimo.com/v1",
14
+ "sgp": "https://token-plan-sgp.xiaomimimo.com/v1",
15
+ "ams": "https://token-plan-ams.xiaomimimo.com/v1",
16
+ }
17
+ DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
18
+ DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
19
+ DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
20
+
21
+
22
+ def normalize_api_url(raw_url):
23
+ """Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
24
+ url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
25
+ if url.endswith("/chat/completions"):
26
+ return url
27
+ return f"{url}/chat/completions"
28
+
29
+
30
+ def is_mimo_token_plan_key(api_key):
31
+ """Return True for Xiaomi MiMo Token Plan keys, which use token-plan base URLs."""
32
+ return str(api_key or "").strip().startswith("tp-")
33
+
34
+
35
+ def default_mimo_api_url(api_key="", cluster=None):
36
+ """Pick the correct MiMo base URL for pay-as-you-go vs Token Plan keys.
37
+
38
+ MiMo uses independent credentials for pay-as-you-go (`sk-*`) and Token Plan
39
+ (`tp-*`). Token Plan keys must be sent to the Token Plan cluster base URL,
40
+ not the pay-as-you-go `api.xiaomimimo.com` endpoint.
41
+ """
42
+ if is_mimo_token_plan_key(api_key):
43
+ cluster_name = (cluster or os.environ.get("MIMO_TOKEN_PLAN_CLUSTER") or DEFAULT_MIMO_TOKEN_PLAN_CLUSTER)
44
+ cluster_name = str(cluster_name).strip().lower()
45
+ return MIMO_TOKEN_PLAN_API_URLS.get(cluster_name, MIMO_TOKEN_PLAN_API_URLS[DEFAULT_MIMO_TOKEN_PLAN_CLUSTER])
46
+ return DEFAULT_MIMO_API_URL
47
+
48
+
49
+ def env_int(name, default, *, minimum=None):
50
+ """Read an integer env var; ignore malformed values instead of crashing import."""
51
+ raw = os.environ.get(name)
52
+ if raw is None or raw == "":
53
+ return default
54
+ try:
55
+ value = int(raw)
56
+ except (TypeError, ValueError):
57
+ return default
58
+ if minimum is not None:
59
+ value = max(minimum, value)
60
+ return value
61
+
62
+
63
+ def env_bool(name, default=False):
64
+ """Read common boolean env var forms."""
65
+ raw = os.environ.get(name)
66
+ if raw is None or raw == "":
67
+ return default
68
+ return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
69
+
70
+
71
+ def env_float(name, default, *, minimum=None):
72
+ """Read a float env var; ignore malformed values instead of crashing import."""
73
+ raw = os.environ.get(name)
74
+ if raw is None or raw == "":
75
+ return default
76
+ try:
77
+ value = float(raw)
78
+ except (TypeError, ValueError):
79
+ return default
80
+ if minimum is not None:
81
+ value = max(minimum, value)
82
+ return value
83
+
84
+
85
+ # Single MiMo credential powers ASR + VLM + TTS. Per-capability overrides
86
+ # (MIMO_VIDEO_API_KEY / MIMO_TTS_API_KEY / MIMO_ASR_API_KEY and their *_API_URL forms)
87
+ # are optional and fall back to MIMO_API_KEY / MIMO_API_URL. Token-Plan keys (tp-*) auto-
88
+ # route to the Token-Plan cluster base URL; pay-as-you-go keys use api.xiaomimimo.com.
89
+
90
+ # Cross-language source: when the original audio is in a language the narration is NOT in
91
+ # (e.g. a Japanese drama recapped in Chinese), the original speech bleeding under the narration
92
+ # is just noise the viewer can't parse — it reads as 怪音. In that mode the original is ducked to
93
+ # near-silent UNDER narration; it still plays full-volume in the original-audio gap blocks, where
94
+ # a single language is fine. Explicit SPEECH_DUCKING_VOLUME / ZONE_DUCKING_VOLUME still override.
95
+ _foreign_source_audio = env_bool("FOREIGN_SOURCE_AUDIO", False)
96
+ _foreign_under_narration_volume = 0.05 # original volume under narration when source audio is foreign
97
+
98
+ CONFIG = {
99
+ "fade_ms": env_int("FADE_MS", 120, minimum=0), # 每段 TTS 淡入淡出(ms);过大会让紧凑的句子一顿一顿,120ms 防爆音又不发闷
100
+ "breath_ms": 250, # 段间呼吸空间(ms);block recap 块内连贯、块间留原声呼吸
101
+ "ducking_mode": "fixed", # fixed | sidechaincompress | none
102
+ "ducking_threshold": 0.15,
103
+ "ducking_ratio": 3,
104
+ "ducking_attack": 10,
105
+ "ducking_release": 300,
106
+ "ducking_level_sc": 2.0,
107
+ "ducking_makeup": 1.2,
108
+ "ducking_narr_weight": 1.5,
109
+ "ducking_orig_volume": env_float("DUCKING_ORIG_VOLUME", 0.3, minimum=0.0), # 解说时原声基准音量
110
+ # Derived report of the FOREIGN_SOURCE_AUDIO knob this skill implements: it selects the
111
+ # ducking volumes below. Declared so callers can see which policy is in effect.
112
+ "foreign_source_audio": _foreign_source_audio,
113
+ "zone_ducking_volume": env_float("ZONE_DUCKING_VOLUME",
114
+ _foreign_under_narration_volume if _foreign_source_audio else 0.12, minimum=0.0), # 解说时原声压低到的音量
115
+ "idle_orig_volume": env_float("IDLE_ORIG_VOLUME", 1.0, minimum=0.0), # 解说块之间的"原声块"音量:默认满音量(1.0),让精彩原声整段放出来,不被压低(用户要求解说成块、原声也成块)
116
+ "duck_fade_seconds": env_float("DUCK_FADE_SECONDS", 0.3, minimum=0.0), # 解说块/原声块切换的淡入淡出(秒),略放宽到 0.3 让满音量↔压低的过渡更顺
117
+ "duck_bridge_seconds": env_float("DUCK_BRIDGE_SECONDS", 1.5, minimum=0.0), # 仅把间隔小于此值的相邻解说窗口并成一段压低;超过则视为作者特意留的"原声块",原声放回满音量。默认 1.5s:解说块内部连续压低,块与块之间的留白放出满音量原声。该值只控制短间隔合并,不设定旁白/原声配额。调大→更连续铺底、原声块更少;调小→更碎
118
+ "bgm_path": os.environ.get("BGM_PATH", "").strip(), # 背景音乐文件(可选),留空则不加 BGM
119
+ "source_video": os.environ.get("SOURCE_VIDEO", "").strip(), # 剪辑模式下的原始视频(可选),用于时间线/剪映导出引用原片片段
120
+ "export_jianying": env_bool("EXPORT_JIANYING", False), # 渲染后可选导出剪映草稿(默认关;与核心解耦)
121
+ "jianying_draft_dir": os.environ.get("JIANYING_DRAFT_DIR", "").strip(), # 剪映草稿输出父目录(留空=work_dir)
122
+ "jianying_bundle_media": env_bool("JIANYING_BUNDLE_MEDIA", True), # 默认开:macOS 剪映沙箱读不到外部路径,须把素材拷进草稿目录
123
+ "bgm_volume": env_float("BGM_VOLUME", 0.18, minimum=0.0), # BGM 铺底音量
124
+ "bgm_ducking_volume": env_float("BGM_DUCKING_VOLUME", 0.10, minimum=0.0), # 旁白时 BGM 压低到的音量
125
+ "narration_speed": env_float("NARRATION_SPEED", 1.15, minimum=0.5), # 解说整体提速(atempo),默认回到可懂区间;长片可设 1.0
126
+ "narration_cumulative_tempo_max": env_float("NARRATION_CUMULATIVE_TEMPO_MAX", 1.35, minimum=1.0), # TTS rate × 全局 atempo × 段内 atempo 的累计上限
127
+ "narration_cumulative_tempo_hard_max": env_float("NARRATION_CUMULATIVE_TEMPO_HARD_MAX", 1.40, minimum=1.0), # QC/阻断硬上限
128
+ "tts_segment_tempo_max": env_float("TTS_SEGMENT_TEMPO_MAX", 1.20, minimum=1.0), # 兼容旧段内 atempo 上限;实际会被累计预算收紧
129
+ "mask_source_subtitles": env_bool("MASK_SOURCE_SUBTITLES", False), # 遮挡原片烧录字幕;必须配合显式 SOURCE_SUBTITLE_MASK_POLICY
130
+ "source_subtitle_mask_policy_declared": bool(os.environ.get("SOURCE_SUBTITLE_MASK_POLICY", "").strip()),
131
+ "source_subtitle_mask_policy": (
132
+ os.environ.get("SOURCE_SUBTITLE_MASK_POLICY", "").strip().lower()
133
+ or "off"
134
+ ), # off | opt_in | safe | forced;MASK_SOURCE_SUBTITLES alone is legacy implicit and QC-blocking
135
+ "source_subtitle_mask_ratio": env_float("SOURCE_SUBTITLE_MASK_RATIO", 0.14, minimum=0.0), # 底部遮挡比例
136
+ "source_subtitle_mask_timing": os.environ.get("SOURCE_SUBTITLE_MASK_TIMING", "narration").strip().lower(), # all | narration;增强版默认仅解说时遮罩
137
+ "subtitle_mask_opacity": min(1.0, env_float("SUBTITLE_MASK_OPACITY", 0.6, minimum=0.0)), # 0=透明,1=全黑;增强版默认半透明
138
+ "subtitle_mask_padding": env_int("SUBTITLE_MASK_PADDING", 4, minimum=0),
139
+ "subtitle_y_top": env_int("SUBTITLE_Y_TOP", -1, minimum=-1), # 自动旋转后的显示画布坐标;top/bot 同时有效时贴合原字幕带
140
+ "subtitle_y_bot": env_int("SUBTITLE_Y_BOT", -1, minimum=-1),
141
+ "narration_delay_seconds": env_float("NARRATION_DELAY_SECONDS", 0.0, minimum=0.0), # 默认严格采用 Agent 写入的 start;旧项目可显式恢复延迟
142
+ "narration_tighten": env_bool("NARRATION_TIGHTEN", True), # 段落内把句子紧贴上一句实际收尾播放,句间间隔稳定≤tight_pause,杜绝"一句解说一段空白"的卡顿
143
+ "narration_run_gap_seconds": env_float("NARRATION_RUN_GAP_SECONDS", 1.6, minimum=0.0), # 作者留白超过此值=新段落(让精彩原声透出);小于则视为同一连续段落
144
+ "narration_tight_pause_seconds": env_float("NARRATION_TIGHT_PAUSE_SECONDS", 0.35, minimum=0.0), # 段落内句间固定间隔(秒)
145
+ "narration_max_pull_seconds": env_float("NARRATION_MAX_PULL_SECONDS", 1.2, minimum=0.0), # 收紧时一句最多比作者标注提前的秒数(漂移上限,越小越贴画面)
146
+ "narration_tail_pad_seconds": 0.1, # 解说尾部最少留白;短 slot 会自动压低 delay 避免截断
147
+ "quiet_overlap_min_ratio": 0.8, # 解说段至少多少比例落在安静窗口内才标记为非对白重叠
148
+ "speech_ducking_volume": env_float("SPEECH_DUCKING_VOLUME",
149
+ _foreign_under_narration_volume if _foreign_source_audio else 0.2, minimum=0.0), # 解说与对白重叠时原声音量
150
+ "burn_subtitles": env_bool("BURN_SUBTITLES", True), # 烧录解说字幕(默认开;遮挡原字幕后需自带字幕,否则字幕区空白)
151
+ "subtitle_original_in_gaps": env_bool("SUBTITLE_ORIGINAL_IN_GAPS", True), # 原声留白处补烧原声台词字幕(来自 ASR)
152
+ "force_video_reencode": env_bool("FORCE_VIDEO_REENCODE", False), # 组装时重编码视频,修复部分容器时间戳问题
153
+ # 成片压制(仅在重编码时生效:烧字幕/遮罩/缩放/FORCE_VIDEO_REENCODE 任一触发重编码)。
154
+ "output_crf": env_int("OUTPUT_CRF", 18, minimum=0), # x264 CRF;越大文件越小、画质越低(18≈视觉无损,23~26 体积更小)
155
+ "output_preset": os.environ.get("OUTPUT_PRESET", "veryfast"), # x264 preset;slow/slower 同 CRF 下体积更小但更慢
156
+ "output_max_height": env_int("OUTPUT_MAX_HEIGHT", 0, minimum=0), # >0 时把成片高度上限缩到该值(保持宽高比、偶数宽);0=不缩放
157
+ # 成片末端整体响度归一(默认混音偏轻,归一后更接近常见短视频响度;样片约 -11.9,默认取更安全的 -14)
158
+ "final_loudnorm": env_bool("FINAL_LOUDNORM", True), # 组装末端做一次整体响度归一
159
+ "target_lufs": env_float("TARGET_LUFS", -14.0), # 目标综合响度 (LUFS)
160
+ "target_true_peak": env_float("TARGET_TRUE_PEAK", -1.0), # 目标真峰值 (dBTP)
161
+ "target_lra": env_float("TARGET_LRA", 11.0), # 目标响度范围 (LU)
162
+ "final_limiter_peak": env_float("FINAL_LIMITER_PEAK", 0.98, minimum=0.1), # loudnorm 后峰值保护 limiter
163
+ "subtitle_font_name": os.environ.get("SUBTITLE_FONT_NAME", "Arial"),
164
+ "subtitle_font_size": env_int("SUBTITLE_FONT_SIZE", 42, minimum=8),
165
+ "subtitle_primary_color": os.environ.get("SUBTITLE_PRIMARY_COLOR", "&H00FFFFFF"),
166
+ "subtitle_outline_color": os.environ.get("SUBTITLE_OUTLINE_COLOR", "&H00000000"),
167
+ "subtitle_outline": env_float("SUBTITLE_OUTLINE", 2.0, minimum=0.0),
168
+ "subtitle_shadow": env_float("SUBTITLE_SHADOW", 1.0, minimum=0.0),
169
+ "subtitle_margin_v": env_int("SUBTITLE_MARGIN_V", 48, minimum=0),
170
+ "subtitle_margin_l": env_int("SUBTITLE_MARGIN_L", 40, minimum=0),
171
+ "subtitle_margin_r": env_int("SUBTITLE_MARGIN_R", 40, minimum=0),
172
+ "subtitle_alignment": env_int("SUBTITLE_ALIGNMENT", 2, minimum=1),
173
+ "subtitle_max_chars": env_int("SUBTITLE_MAX_CHARS", 20, minimum=6),
174
+ "subtitle_max_lines": env_int("SUBTITLE_MAX_LINES", 2, minimum=1),
175
+ "subtitle_play_res_x": env_int("SUBTITLE_PLAY_RES_X", 1280, minimum=1),
176
+ "subtitle_play_res_y": env_int("SUBTITLE_PLAY_RES_Y", 720, minimum=1),
177
+ }
178
+ if isinstance(_EXISTING_CONFIG_REF, dict):
179
+ _EXISTING_CONFIG_REF.clear()
180
+ _EXISTING_CONFIG_REF.update(CONFIG)
181
+ CONFIG = _EXISTING_CONFIG_REF
182
+
183
+ SCRIPT_DIR = Path(__file__).parent
184
+ PROMPTS_DIR = SCRIPT_DIR.parent / "references"
185
+
186
+ def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
187
+ """Return the canonical tempo budget shared by voiceover and assemble.
188
+
189
+ `effective_tempo` is the user-perceived cumulative compression:
190
+ TTS rate × global narration atempo × per-segment atempo. The segment atempo
191
+ cap is therefore tightened by the configured global speed and TTS rate
192
+ offset; callers must fail/shorten instead of time-trimming speech when the
193
+ needed ratio exceeds `segment_tempo_max`.
194
+ """
195
+ cfg = config or CONFIG
196
+ global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
197
+ rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
198
+ cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
199
+ hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
200
+ legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
201
+ segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
202
+ return {
203
+ "global_narration_speed": global_speed,
204
+ "tts_rate_factor": rate_factor,
205
+ "cumulative_tempo_max": cumulative_max,
206
+ "cumulative_tempo_hard_max": hard_max,
207
+ "segment_tempo_max": segment_tempo_max,
208
+ "max_raw_duration_factor": global_speed * segment_tempo_max,
209
+ }
210
+
211
+ def log(msg):
212
+ print(f"[video-recap] {msg}", flush=True)
213
+
214
+ def run_cmd(cmd, **kwargs):
215
+ """运行命令,返回 CompletedProcess"""
216
+ if isinstance(cmd, list):
217
+ display_parts = []
218
+ for part in cmd:
219
+ text = str(part)
220
+ display_parts.append(text if len(text) <= 240 else text[:237] + "...")
221
+ display = " ".join(display_parts)
222
+ else:
223
+ display = str(cmd)
224
+ if len(display) > 2000:
225
+ display = display[:1997] + "..."
226
+ log(f"运行: {display}")
227
+ return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
228
+
229
+ def get_video_duration(video_path):
230
+ """获取视频时长(秒)"""
231
+ cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
232
+ "-of", "csv=p=0", str(video_path)]
233
+ result = run_cmd(cmd)
234
+ if result.returncode != 0:
235
+ return 0.0
236
+ try:
237
+ return float(result.stdout.strip())
238
+ except (TypeError, ValueError):
239
+ return 0.0