@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,795 @@
1
+ """Own recap timeline artifacts, continuation state, and cut QC surfaces."""
2
+
3
+ import hashlib
4
+ import json
5
+ import math
6
+ import os
7
+ import shlex
8
+ import sys
9
+ from pathlib import Path
10
+ from recap_runtime import (
11
+ _coerce_videos,
12
+ _entry,
13
+ _load_json,
14
+ _load_run_manifest,
15
+ _multi_run_manifest_payload,
16
+ _run_manifest_payload,
17
+ )
18
+
19
+ ASSEMBLY_MANIFEST = "assembly_manifest.json"
20
+
21
+ PHASE_LEDGER = "recap_phase.json"
22
+
23
+ CUT_TIMELINE_CRAFT_BULLETS = [
24
+ "- 片段顺序必须服务同一条故事主线,而不是无序高光;可使用 0–1 个 cold open,随后回到 setup → turn → escalation → payoff。",
25
+ "- 每个片段必须对应 `recap_story_plan.json` 中的一个 change-based beat;删除后不损失因果、人物或情绪的片段通常不保留。",
26
+ "- `reason` 统一写成 `beat_id | function | change | POV | preferred moment | 入点 | 出点`,不能只写 hook、重要剧情或事件摘要。",
27
+ "- 优先保留因果、揭示、决定、关系移动、情绪转向与不可替代的表演/反应;跳过片尾、广告、重复静态画面和水印废片段。",
28
+ "- 片段追求最短但完整:建立镜头可以短,关键表演/反应允许多停一点;在完整台词、完整动作或自然声音边界结束,避免原声从半句中切入或切出。",
29
+ "- 新人物/地点/情绪场景先给画面建立空间,再进旁白;不要在原片叠化或闪白中间再切一次。scene score 只用于定位候选接点,最终必须播放接点前后判断。",
30
+ "- 对短时间内密集的 scene 候选先区分来源:原片的无关短镜头整段删除,相关短镜头扩展到完整动作/反应;本次拼接制造的切点优先移动边界、恢复同源连续运动或合并片段,能消除就不保留。",
31
+ ]
32
+
33
+ MULTI_SOURCE_NARRATION_CRAFT_BULLETS = [
34
+ "- `narration.json` 只使用 edited_source.mp4 的 OUTPUT 时间线,不使用任一原片时间。",
35
+ "- 先为每个 beat 指定 `audio_owner`,再决定是否需要旁白;允许 original_dialogue、action_sound、ambience、music、silence 或 narration 主导。",
36
+ "- 旁白只承担 context、causal_link、foreshadow、interpretation 或 transition;`narration_job=none` 的 beat 不写旁白。",
37
+ "- 7:3 不是配额,只是素材无法给出更好判断时的粗略回退;强对白、动作声、环境或沉默可以完整拥有一个 beat。",
38
+ "- 旁白拥有 beat 时才写成一个连续思路的 BLOCK;句数不是目标,字幕拆 cue 也不能切碎 TTS。不要把每个 beat 都机械变成旁白块或固定原声留白。",
39
+ "- 使用 kept-clip map 与每个 source work_dir 核对人物、事实、ASR 和上下文;跨源转场必须有明确叙事任务。",
40
+ ]
41
+
42
+ _ALLOWED_VISUAL_OVERLAY_TYPES = {"top_title", "inline_label_or_callout"}
43
+
44
+ _VISUAL_OVERLAYS = "visual_overlays.json"
45
+
46
+
47
+ def _finite_number(value):
48
+ try:
49
+ number = float(value)
50
+ except (TypeError, ValueError):
51
+ return None
52
+ return number if math.isfinite(number) else None
53
+
54
+
55
+ def _iter_narration_segments(narration_data):
56
+ if isinstance(narration_data, list):
57
+ yield from (item for item in narration_data if isinstance(item, dict))
58
+ return
59
+ if not isinstance(narration_data, dict):
60
+ return
61
+ for key in ("segments", "narration", "items"):
62
+ value = narration_data.get(key)
63
+ if isinstance(value, list):
64
+ yield from (item for item in value if isinstance(item, dict))
65
+ return
66
+
67
+
68
+ def _canonical_visual_overlay(overlay, segment=None):
69
+ """Return the first-release assemble overlay contract, or None.
70
+
71
+ Recap owns the handoff artifact but does not invent richer overlay semantics. Only the
72
+ two overlay types implemented by assemble are allowed through; all unknown platform or
73
+ future types stay out of the canonical first-release file.
74
+ """
75
+ if not isinstance(overlay, dict):
76
+ return None
77
+ typ = overlay.get("type")
78
+ if typ not in _ALLOWED_VISUAL_OVERLAY_TYPES:
79
+ return None
80
+ text = overlay.get("text")
81
+ if text is None or str(text) == "":
82
+ return None
83
+ seg = segment if isinstance(segment, dict) else {}
84
+ start = _finite_number(overlay.get("start"))
85
+ end = _finite_number(overlay.get("end"))
86
+ if start is None:
87
+ start = _finite_number(seg.get("start"))
88
+ if end is None:
89
+ end = _finite_number(seg.get("end"))
90
+ if start is None or end is None or end <= start:
91
+ return None
92
+
93
+ item = {"type": typ, "text": str(text), "start": start, "end": end}
94
+ # Preserve renderer-supported placement hints supplied by upstream facts; do not add new
95
+ # semantic overlay kinds or infer platform-specific cards/chapters.
96
+ for key in ("anchor", "x", "y", "max_width", "style"):
97
+ if key in overlay:
98
+ item[key] = overlay[key]
99
+ return item
100
+
101
+
102
+ def _extract_visual_overlays_from_narration(narration_path):
103
+ data = _load_json(narration_path)
104
+ overlays = []
105
+ for segment in _iter_narration_segments(data):
106
+ raw = segment.get("visual_overlays")
107
+ if not isinstance(raw, list):
108
+ continue
109
+ for overlay in raw:
110
+ item = _canonical_visual_overlay(overlay, segment)
111
+ if item is not None:
112
+ overlays.append(item)
113
+ return overlays
114
+
115
+
116
+ def _write_canonical_visual_overlays(work_dir, narration_path):
117
+ """Write assemble's canonical work_dir/visual_overlays.json recap handoff.
118
+
119
+ Direct assemble still supports user-authored/manual visual_overlays.json files. Once
120
+ recap owns the handoff for a narration, however, the canonical artifact must be a
121
+ deterministic reflection of the current narration so reused work_dirs cannot render
122
+ stale overlays from a previous run. Unsupported/future overlay types are filtered out
123
+ and represented as an explicit empty overlay list.
124
+ """
125
+ work_dir = Path(work_dir)
126
+ path = work_dir / _VISUAL_OVERLAYS
127
+ overlays = _extract_visual_overlays_from_narration(narration_path)
128
+ payload = {"schema_version": 1, "overlays": overlays}
129
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
130
+ print(f"[video-recap] 🧩 visual overlays: {len(overlays)} → {path}", flush=True)
131
+ return path
132
+
133
+
134
+ def _print_grounding_qc_pointer(work_dir):
135
+ qc_path = Path(work_dir) / "grounding_qc.json"
136
+ if not qc_path.exists():
137
+ return
138
+ data = _load_json(qc_path)
139
+ if isinstance(data, dict):
140
+ verdict = data.get("verdict", "unknown")
141
+ ranges = (data.get("review_coverage") or {}).get("time_ranges") or []
142
+ warnings = data.get("warnings") or []
143
+ suffix = f" · warnings {len(warnings)}" if warnings else ""
144
+ print(
145
+ f"[video-recap] 🧭 Grounding QC: {verdict} · ranges {len(ranges)}{suffix} → {qc_path}",
146
+ flush=True,
147
+ )
148
+ else:
149
+ print(f"[video-recap] 🧭 Grounding QC → {qc_path}", flush=True)
150
+
151
+
152
+ def _print_narration_review_pointer(work_dir, *, review_ran=True):
153
+ """Surface the advisory narration review produced by this run, if any.
154
+
155
+ Review is optional/fail-open. Avoid surfacing a stale narration_review.md from an
156
+ older run when review was disabled or failed before producing fresh artifacts.
157
+ """
158
+ _print_grounding_qc_pointer(work_dir)
159
+ if not review_ran:
160
+ return
161
+ review_md = Path(work_dir) / "narration_review.md"
162
+ if not review_md.exists():
163
+ return
164
+ data = _load_json(Path(work_dir) / "narration_review.json")
165
+ if isinstance(data, dict):
166
+ findings = [f for f in (data.get("findings") or []) if isinstance(f, dict)]
167
+ n_err = sum(1 for f in findings if f.get("severity") == "error")
168
+ tag = str(data.get("verdict") or "见文件")
169
+ print(
170
+ f"[video-recap] 📋 解说评审(建议性,不拦截): {tag} · "
171
+ f"{len(findings)} 条意见(error {n_err})→ {review_md}"
172
+ )
173
+ else:
174
+ print(f"[video-recap] 📋 解说评审意见 → {review_md}")
175
+
176
+
177
+ def _settings_for_compare(settings):
178
+ """Settings that, if changed, invalidate reusing an existing work_dir on resume.
179
+
180
+ `consolidate`/`consolidate_asr` are EXCLUDED: they only ADD an optional understanding
181
+ artifact and never re-run Phase A on a Phase-B resume, so a stored manifest carrying the
182
+ old default (or missing the key entirely, pre-dating it) must still resume — otherwise
183
+ flipping `--consolidate`'s default ON would hard-fail every in-flight work_dir.
184
+ """
185
+ s = dict(settings or {})
186
+ s.pop("consolidate", None)
187
+ s.pop("consolidate_asr", None)
188
+ return s
189
+
190
+
191
+ def _manifest_mismatches(work_dir, video, args):
192
+ expected = _run_manifest_payload(video, args)
193
+ actual = _load_run_manifest(work_dir)
194
+ if not actual:
195
+ return [
196
+ "缺少或无法读取 recap_run_manifest.json;不能证明 work_dir 属于当前视频/参数"
197
+ ]
198
+ mismatches = []
199
+ for key in ("source_video", "source_video_fingerprint"):
200
+ if actual.get(key) != expected.get(key):
201
+ mismatches.append(
202
+ f"{key}: expected {expected.get(key)!r}, got {actual.get(key)!r}"
203
+ )
204
+ if _settings_for_compare(actual.get("settings")) != _settings_for_compare(
205
+ expected.get("settings")
206
+ ):
207
+ mismatches.append("settings: 当前 CLI/env 参数与 Phase A manifest 不匹配")
208
+ return mismatches
209
+
210
+
211
+ def _multi_manifest_mismatches(work_dir, videos, args, source_records):
212
+ expected = _multi_run_manifest_payload(videos, args, source_records)
213
+ actual = _load_run_manifest(work_dir)
214
+ if not actual:
215
+ return [
216
+ "缺少或无法读取 recap_run_manifest.json;不能证明 work_dir 属于当前多视频/参数"
217
+ ]
218
+ mismatches = []
219
+ if actual.get("mode") != "multi_source":
220
+ mismatches.append(f"mode: expected 'multi_source', got {actual.get('mode')!r}")
221
+ expected_sources = [
222
+ {
223
+ "source_id": s.get("source_id"),
224
+ "source_path": s.get("source_path"),
225
+ "source_video_fingerprint": s.get("source_video_fingerprint"),
226
+ }
227
+ for s in expected.get("sources", [])
228
+ ]
229
+ actual_sources = [
230
+ {
231
+ "source_id": s.get("source_id"),
232
+ "source_path": s.get("source_path"),
233
+ "source_video_fingerprint": s.get("source_video_fingerprint"),
234
+ }
235
+ for s in actual.get("sources", [])
236
+ if isinstance(s, dict)
237
+ ]
238
+ if actual_sources != expected_sources:
239
+ mismatches.append(
240
+ "sources: 当前输入视频列表/顺序/source_id/fingerprint 与 Phase A manifest 不匹配"
241
+ )
242
+ if _settings_for_compare(actual.get("settings")) != _settings_for_compare(
243
+ expected.get("settings")
244
+ ):
245
+ mismatches.append("settings: 当前 CLI/env 参数与 Phase A manifest 不匹配")
246
+ return mismatches
247
+
248
+
249
+ def _read_assembly_output(work_dir):
250
+ manifest = _load_json(Path(work_dir) / ASSEMBLY_MANIFEST)
251
+ if isinstance(manifest, dict) and manifest.get("final_output"):
252
+ return Path(manifest["final_output"])
253
+ return None
254
+
255
+
256
+ def _file_md5(path):
257
+ path = Path(path)
258
+ return hashlib.md5(path.read_bytes()).hexdigest() if path.exists() else None
259
+
260
+
261
+ def _read_phase_ledger(work_dir):
262
+ """Phase ledger (cut mode): which artifacts exist and the clip_plan/narration they match.
263
+
264
+ Lets resume be driven by recorded phase state rather than bare file existence — the
265
+ prerequisite for the cut-first/narrate-second two-pause flow, and the guard that keeps a
266
+ narration written for one clip_plan from silently driving a different cut into TTS.
267
+ """
268
+ ledger = _load_json(Path(work_dir) / PHASE_LEDGER)
269
+ return ledger if isinstance(ledger, dict) else None
270
+
271
+
272
+ def _write_phase_ledger(work_dir, **fields):
273
+ ledger = _read_phase_ledger(work_dir) or {}
274
+ ledger.update(fields)
275
+ (Path(work_dir) / PHASE_LEDGER).write_text(
276
+ json.dumps(ledger, ensure_ascii=False, indent=2), encoding="utf-8"
277
+ )
278
+ return ledger
279
+
280
+
281
+ def _cut_narration_is_stale(ledger, current_clip_plan_fp):
282
+ """Two-pass cut: the narration is authored against the rendered cut shown at the A2 pause,
283
+ i.e. against the clip_plan recorded in the ledger. If clip_plan changed since (a re-cut)
284
+ while that narration is still present, it describes the OLD cut — stale."""
285
+ if not ledger:
286
+ return False
287
+ recorded_cp = ledger.get("clip_plan_fingerprint")
288
+ return bool(recorded_cp is not None and recorded_cp != current_clip_plan_fp)
289
+
290
+
291
+ def _continuation_command(video, work_dir, args):
292
+ videos = _coerce_videos(video)
293
+ parts = [
294
+ sys.executable,
295
+ str(_entry("video-recap", "recap.py")),
296
+ *[str(v) for v in videos],
297
+ "--work-dir",
298
+ str(work_dir),
299
+ ]
300
+ if args.context:
301
+ parts += ["--context", args.context]
302
+ if args.scene_threshold is not None:
303
+ parts += ["--scene-threshold", str(args.scene_threshold)]
304
+ if args.style != "纪录片":
305
+ parts += ["--style", args.style]
306
+ if args.edit_mode != "full":
307
+ parts += ["--edit-mode", args.edit_mode]
308
+ if args.target_duration:
309
+ parts += ["--target-duration", args.target_duration]
310
+ if getattr(args, "allow_duration_drift", False):
311
+ parts.append("--allow-duration-drift")
312
+ if getattr(args, "allow_sparse_cut", False):
313
+ parts.append("--allow-sparse-cut")
314
+ if args.skip_asr:
315
+ parts.append("--skip-asr")
316
+ if args.mimo_video_overview:
317
+ parts.append("--mimo-video-overview")
318
+ mimo_mode = getattr(args, "mimo_qc", "off")
319
+ if mimo_mode != "off":
320
+ parts += ["--mimo-qc", mimo_mode]
321
+ if getattr(args, "mimo_qc_refresh", False):
322
+ parts.append("--mimo-qc-refresh")
323
+ if not args.consolidate: # default is ON now; only the opt-out needs to round-trip
324
+ parts.append("--no-consolidate")
325
+ if args.consolidate_asr:
326
+ parts.append("--consolidate-asr")
327
+ if getattr(args, "mimo_tts_voice", None):
328
+ parts += ["--mimo-tts-voice", args.mimo_tts_voice]
329
+ if getattr(args, "tts_provider", "auto") != "auto":
330
+ parts += ["--tts-provider", args.tts_provider]
331
+ if getattr(args, "voice_ref", None):
332
+ parts += ["--voice-ref", args.voice_ref]
333
+ if getattr(args, "allow_partial_tts", False):
334
+ parts.append("--allow-partial-tts")
335
+ if getattr(args, "burn_subtitles", None) is not None:
336
+ parts.append(
337
+ "--burn-subtitles" if args.burn_subtitles else "--no-burn-subtitles"
338
+ )
339
+ if getattr(args, "subtitle_y_top", None) is not None:
340
+ parts += ["--subtitle-y-top", str(args.subtitle_y_top)]
341
+ if getattr(args, "subtitle_y_bot", None) is not None:
342
+ parts += ["--subtitle-y-bot", str(args.subtitle_y_bot)]
343
+ if getattr(args, "output_dir", None):
344
+ parts += ["--output-dir", args.output_dir]
345
+ if getattr(args, "export_jianying", False):
346
+ parts.append("--export-jianying")
347
+ if getattr(args, "jianying_bundle_media", False):
348
+ parts.append("--jianying-bundle-media")
349
+ if getattr(args, "jianying_no_bundle_media", False):
350
+ parts.append("--jianying-no-bundle-media")
351
+ if getattr(args, "review_narration", None) is not None:
352
+ parts.append(
353
+ "--review-narration" if args.review_narration else "--no-review-narration"
354
+ )
355
+ if getattr(args, "require_narration_review", False):
356
+ parts.append("--require-narration-review")
357
+ if getattr(args, "material_library_dir", None):
358
+ parts += ["--material-library-dir", args.material_library_dir]
359
+ if getattr(args, "use_materials", False):
360
+ parts.append("--use-materials")
361
+ if getattr(args, "save_materials", False):
362
+ parts.append("--save-materials")
363
+ return " ".join(shlex.quote(part) for part in parts)
364
+
365
+
366
+ def _source_work_dir(project_work_dir, source_record):
367
+ return Path(project_work_dir) / source_record["source_work_dir"]
368
+
369
+
370
+ def _understand_args_for_source(source_record, source_work_dir, args):
371
+ uargs = [
372
+ source_record["source_path"],
373
+ "--work-dir",
374
+ str(source_work_dir),
375
+ "--style",
376
+ args.style,
377
+ ]
378
+ if args.context:
379
+ uargs += ["--context", args.context]
380
+ if args.scene_threshold is not None:
381
+ uargs += ["--scene-threshold", str(args.scene_threshold)]
382
+ if args.edit_mode:
383
+ uargs += ["--edit-mode", args.edit_mode]
384
+ if args.target_duration:
385
+ uargs += ["--target-duration", args.target_duration]
386
+ if args.skip_asr:
387
+ uargs.append("--skip-asr")
388
+ if args.mimo_video_overview:
389
+ uargs.append("--mimo-video-overview")
390
+ uargs.append("--consolidate" if args.consolidate else "--no-consolidate")
391
+ if args.consolidate_asr:
392
+ uargs.append("--consolidate-asr")
393
+ return uargs
394
+
395
+
396
+ def _brief_excerpt(path, limit=1200):
397
+ try:
398
+ text = Path(path).read_text(encoding="utf-8")
399
+ except OSError:
400
+ return ""
401
+ if limit <= 0:
402
+ return ""
403
+ if len(text) <= limit:
404
+ return text
405
+ if limit < 8:
406
+ return text[:limit]
407
+
408
+ def section(heading):
409
+ lines = text.splitlines()
410
+ try:
411
+ start = lines.index(heading)
412
+ except ValueError:
413
+ return ""
414
+ end = next(
415
+ (
416
+ index
417
+ for index in range(start + 1, len(lines))
418
+ if lines[index].startswith("## ")
419
+ ),
420
+ len(lines),
421
+ )
422
+ return "\n".join(lines[start:end]).strip()
423
+
424
+ def clipped(value, budget):
425
+ if len(value) <= budget:
426
+ return value
427
+ marker = "\n…\n"
428
+ if budget <= len(marker) + 1:
429
+ return value[:budget]
430
+ usable = max(0, budget - len(marker))
431
+ head = max(1, round(usable * 0.7))
432
+ tail = max(0, usable - head)
433
+ suffix = value[-tail:].lstrip() if tail else ""
434
+ return value[:head].rstrip() + marker + suffix
435
+
436
+ # The generic writing contract occupies the front of every generated brief. A raw
437
+ # prefix therefore drops the source facts that multi-source planning actually needs.
438
+ # Give evidence-bearing sections equal space and retain both their opening and tail.
439
+ preferred = [
440
+ section("## Story context (from background_research.json)"),
441
+ section("## Understanding index (from consolidate.py)"),
442
+ section("## ASR writing chunks (semantic windows)"),
443
+ section("## Scene timing guide"),
444
+ ]
445
+ preferred = [value for value in preferred if value]
446
+ if preferred:
447
+ separators = 2 * (len(preferred) - 1)
448
+ per_section = max(1, (limit - separators) // len(preferred))
449
+ excerpt = "\n\n".join(clipped(value, per_section) for value in preferred)
450
+ return excerpt[:limit]
451
+
452
+ marker = "\n…\n"
453
+ usable = max(0, limit - len(marker))
454
+ head = max(1, usable // 4)
455
+ tail = max(0, usable - head)
456
+ suffix = text[-tail:].lstrip() if tail else ""
457
+ return text[:head].rstrip() + marker + suffix
458
+
459
+
460
+ def _write_multi_source_clip_brief(work_dir, source_records, args):
461
+ lines = [
462
+ "# Multi-source Clip Plan Brief",
463
+ "",
464
+ "你正在做多视频剪辑复盘。当前 MVP 只支持 `--edit-mode cut`:先做跨素材的故事与视听决定,再写 `clip_plan.json`;下一步会剪出 `edited_source.mp4`,最后按 OUTPUT 时间轴写 `narration.json`。",
465
+ "",
466
+ "## 创作决定",
467
+ "",
468
+ "先判断创作控制模式:CREATE 比较至少两个可行剪辑假设;DIRECTED 落实用户指定结构;REVISION 只改最新反馈点并冻结未点名内容。然后写或更新 `recap_story_plan.json` 与 `visual_audio_board.json`。前者记录观众承诺、POV、戏剧问题、选定主线及 change-based beats;后者记录每拍的具体画面/反应、入点/出点、原声锚点、`audio_owner` 与 `narration_job`。",
469
+ "",
470
+ "多视频不是把每个来源各做一段小总结。每个来源片段都必须服务同一条主线,并用 `source_id` 保留证据归属。这两份计划是 Agent 与建议型评审使用的工作记录,不是 CLI 渲染门禁。",
471
+ "",
472
+ "## 必须写入的格式",
473
+ "",
474
+ "```json",
475
+ '{"target_duration":"10m","clips":[{"source_id":"src_xxx","start":12.0,"end":38.0,"reason":"b01 | hook | knowledge: unknown→threat | POV=主角 | 保留倾听反应 | 入点=问题已问出 | 出点=沉默落地"}]}',
476
+ "```",
477
+ "",
478
+ "- 每个 clip 必须带 `source_id`。",
479
+ "- `start`/`end` 是对应 source 原视频时间(秒)。",
480
+ "- 不同 `source_id` 的相同时间段不算重叠;同一 `source_id` 内不要重复/重叠,除非你明确接受稀疏/重复剪辑风险。",
481
+ '- 素材库是文件系统 JSON/MD/JSONL;需要找历史素材时直接 `grep -R "关键词" <material-library-dir>`。',
482
+ "",
483
+ "## 剪辑规则",
484
+ *CUT_TIMELINE_CRAFT_BULLETS,
485
+ "- 跨来源选择必须服务共享主线;除非本来就是 setup / turn / payoff 的设计,不要让某个来源变成脱节的小复盘。",
486
+ "- 选片前使用下方 source work_dir(`sources/<source_id>`)核对 scenes.json、ASR、索引和逐来源 brief。",
487
+ ]
488
+ if args.target_duration:
489
+ lines.append(f"- 目标时长:`{args.target_duration}`。")
490
+ lines += ["", "## Sources", ""]
491
+ for s in source_records:
492
+ swd = _source_work_dir(work_dir, s)
493
+ lines += [
494
+ f"### {s['source_id']} — {s['source_name']}",
495
+ f"- path: `{s['source_path']}`",
496
+ f"- work_dir: `{swd}`",
497
+ f"- fingerprint: `{s['source_video_fingerprint']}`",
498
+ f"- material_id: `{s.get('material_id')}`",
499
+ ]
500
+ excerpt = _brief_excerpt(swd / "agent_narration_brief.md")
501
+ if excerpt:
502
+ lines += ["", "#### per-source brief excerpt", "", excerpt, ""]
503
+ (Path(work_dir) / "agent_narration_brief.md").write_text(
504
+ "\n".join(lines).rstrip() + "\n", encoding="utf-8"
505
+ )
506
+
507
+
508
+ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
509
+ """Map each source's speech and quiet evidence onto the combined output clock."""
510
+ clips = plan.get("clips", []) if isinstance(plan, dict) else []
511
+ source_by_id = {str(row["source_id"]): row for row in source_records}
512
+ cache = {}
513
+
514
+ def load_source(source_id):
515
+ if source_id in cache:
516
+ return cache[source_id]
517
+ record = source_by_id.get(source_id)
518
+ source_dir = _source_work_dir(work_dir, record) if record else None
519
+ anchors = _load_json(source_dir / "speech_boundary_anchors.json") if source_dir else None
520
+ speech = None
521
+ # Same precedence as audio_mix._handoff_speech_evidence and
522
+ # sentence_boundaries._load_source_speech_spans: the cleaned transcript wins.
523
+ # Reading these in the opposite order made the multi-source output timeline
524
+ # derive its speech spans from a different artifact than every other consumer.
525
+ for name in ("asr_clean.json", "asr_result.json"):
526
+ speech = _load_json(source_dir / name) if source_dir else None
527
+ rows = speech.get("segments", []) if isinstance(speech, dict) else speech
528
+ if isinstance(rows, list) and any(
529
+ isinstance(row, dict) and str(row.get("text") or "").strip()
530
+ for row in rows
531
+ ):
532
+ break
533
+ if isinstance(speech, dict):
534
+ speech = speech.get("segments", [])
535
+ quiet = _load_json(source_dir / "silence_periods.json") if source_dir else None
536
+ cache[source_id] = (
537
+ anchors.get("sentence_anchors", []) if isinstance(anchors, dict) else [],
538
+ speech if isinstance(speech, list) else [],
539
+ quiet if isinstance(quiet, list) else [],
540
+ )
541
+ return cache[source_id]
542
+
543
+ mapped_anchors, mapped_speech, mapped_quiet = [], [], []
544
+ for clip in clips:
545
+ source_id = str(clip.get("source_id") or "")
546
+ try:
547
+ source_start = float(clip["source_start"])
548
+ source_end = float(clip["source_end"])
549
+ output_start = float(clip["output_start"])
550
+ except (KeyError, TypeError, ValueError):
551
+ continue
552
+ anchors, speech_rows, quiet_rows = load_source(source_id)
553
+ for anchor in anchors:
554
+ try:
555
+ when = float(anchor.get("time"))
556
+ except (AttributeError, TypeError, ValueError):
557
+ continue
558
+ if source_start - 0.05 <= when <= source_end + 0.05:
559
+ item = dict(anchor)
560
+ try:
561
+ pause = float(item.get("pause_start", when))
562
+ except (TypeError, ValueError):
563
+ pause = when
564
+ if not math.isfinite(pause):
565
+ pause = when
566
+ pause = max(source_start, min(pause, when))
567
+ item.update(
568
+ source_id=source_id,
569
+ source_time=round(when, 3),
570
+ time=round(output_start + when - source_start, 3),
571
+ source_pause_start=round(pause, 3),
572
+ pause_start=round(output_start + pause - source_start, 3),
573
+ )
574
+ mapped_anchors.append(item)
575
+ for rows, destination, require_text in (
576
+ (speech_rows, mapped_speech, True),
577
+ (quiet_rows, mapped_quiet, False),
578
+ ):
579
+ for row in rows:
580
+ if not isinstance(row, dict):
581
+ continue
582
+ if require_text and not str(row.get("text") or "").strip():
583
+ continue
584
+ if not require_text and bool(row.get("has_speech", False)):
585
+ continue
586
+ try:
587
+ start = max(source_start, float(row["start"]))
588
+ end = min(source_end, float(row["end"]))
589
+ except (KeyError, TypeError, ValueError):
590
+ continue
591
+ if end <= start:
592
+ continue
593
+ item = dict(row)
594
+ item.update(
595
+ source_id=source_id,
596
+ source_start=round(start, 3),
597
+ source_end=round(end, 3),
598
+ start=round(output_start + start - source_start, 3),
599
+ end=round(output_start + end - source_start, 3),
600
+ )
601
+ destination.append(item)
602
+
603
+ payload = {
604
+ "schema_version": 2,
605
+ "artifact": "speech_boundary_anchors_output.json",
606
+ "timeline": "cut_output",
607
+ "source_artifact": "multi_source_manifest.json",
608
+ "clip_plan_fingerprint": hashlib.md5(
609
+ json.dumps(
610
+ plan, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str
611
+ ).encode("utf-8")
612
+ ).hexdigest(),
613
+ "sentence_anchors": sorted(mapped_anchors, key=lambda row: float(row["time"])),
614
+ "speech_spans": sorted(mapped_speech, key=lambda row: (row["start"], row["end"])),
615
+ "quiet_windows": sorted(mapped_quiet, key=lambda row: (row["start"], row["end"])),
616
+ }
617
+ Path(work_dir, "speech_boundary_anchors_output.json").write_text(
618
+ json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
619
+ )
620
+ return payload
621
+
622
+
623
+ def _write_multi_source_output_brief(work_dir, source_records, validated_plan_path):
624
+ plan = _load_json(validated_plan_path)
625
+ clips = plan.get("clips", []) if isinstance(plan, dict) else []
626
+ source_by_id = {s["source_id"]: s for s in source_records}
627
+ speech_evidence = _write_multi_source_output_speech_evidence(
628
+ work_dir, source_records, plan
629
+ )
630
+ lines = [
631
+ "# Multi-source Output Narration Brief",
632
+ "",
633
+ "现在 `edited_source.mp4` 已经由多个源视频剪好。请对剪后成片的 OUTPUT 时间轴写 `narration.json`。",
634
+ "",
635
+ "## 更新创作决定",
636
+ "",
637
+ "先查看 `edited_source.mp4` 与下方 kept-clip map,在 `visual_audio_board.json` 中补齐 OUTPUT 起止时间,并根据实际成片重新确认每拍的 `audio_owner`、原声锚点与 `narration_job`。如剪后顺序改变了主线或情绪路径,同时更新 `recap_story_plan.json`。",
638
+ "",
639
+ "beat 对应关系保留在视听板中;`narration.json` 仍只承载时间、文本与朗读参数,CLI 不声称校验计划映射。",
640
+ "",
641
+ "## narration.json 格式",
642
+ "",
643
+ "```json",
644
+ '[{"start":0.0,"end":4.0,"narration":"解说文本。","pause_after_ms":250,"overlaps_speech":true,"emotion":"平静"}]',
645
+ "```",
646
+ "",
647
+ "注意:`start`/`end` 是剪后成片时间,不是原视频时间。",
648
+ "",
649
+ "## 输出时间线写作规则",
650
+ *MULTI_SOURCE_NARRATION_CRAFT_BULLETS,
651
+ "- 旁白增加上下文、因果、预期、证据支持的解释或跨源过渡,不复述画面像素;先保人物与原声,再润色句子。",
652
+ "",
653
+ "## Kept clips (output → source)",
654
+ ]
655
+ for c in clips:
656
+ sid = c.get("source_id")
657
+ src = source_by_id.get(sid, {})
658
+ lines.append(
659
+ f"- output {_fmt_range(c.get('output_start'), c.get('output_end'))} → "
660
+ f"{sid} `{src.get('source_path', c.get('source_path', ''))}` "
661
+ f"source {_fmt_range(c.get('source_start'), c.get('source_end'))} "
662
+ f"{('— ' + str(c.get('reason'))) if c.get('reason') else ''}"
663
+ )
664
+ anchors = speech_evidence["sentence_anchors"]
665
+ if anchors:
666
+ lines += ["", "## 原声句末安全切入点"]
667
+ lines.extend(
668
+ f"- {float(row['time']):.3f}s ({row.get('source_id')})"
669
+ for row in anchors
670
+ if row.get("confidence") in {"high", "medium"}
671
+ )
672
+ lines += ["", "## Source work dirs"]
673
+ for s in source_records:
674
+ lines.append(f"- {s['source_id']}: `{_source_work_dir(work_dir, s)}`")
675
+ (Path(work_dir) / "agent_narration_brief.md").write_text(
676
+ "\n".join(lines).rstrip() + "\n", encoding="utf-8"
677
+ )
678
+
679
+
680
+ def _qc_status_is_blocking(value):
681
+ if isinstance(value, str):
682
+ return value.strip().lower() in {
683
+ "blocking",
684
+ "blocked",
685
+ "fail",
686
+ "failed",
687
+ "error",
688
+ }
689
+ return bool(value) if isinstance(value, bool) else False
690
+
691
+
692
+ def _cut_qc_blocking_reasons(qc):
693
+ if not isinstance(qc, dict):
694
+ return []
695
+ reasons = []
696
+ for key in (
697
+ "status",
698
+ "target_duration_status",
699
+ "duration_status",
700
+ "boundary_status",
701
+ ):
702
+ if _qc_status_is_blocking(qc.get(key)):
703
+ reasons.append(f"{key}={qc.get(key)}")
704
+ for key in ("blocking", "blocked", "failed", "fail", "errors"):
705
+ value = qc.get(key)
706
+ if value:
707
+ reasons.append(f"{key}={value}")
708
+ checks = qc.get("checks") if isinstance(qc.get("checks"), list) else []
709
+ for item in checks:
710
+ if isinstance(item, dict) and _qc_status_is_blocking(item.get("status")):
711
+ reasons.append(f"{item.get('name', 'check')}={item.get('status')}")
712
+ return reasons
713
+
714
+
715
+ def _cut_qc_summary_lines(qc):
716
+ if not isinstance(qc, dict) or not qc:
717
+ return []
718
+ parts = []
719
+ for key in (
720
+ "target_duration_status",
721
+ "total_duration",
722
+ "clip_count",
723
+ "join_fade_ms",
724
+ ):
725
+ if key in qc:
726
+ parts.append(f"{key}={qc.get(key)}")
727
+ geometry = qc.get("output_geometry")
728
+ if isinstance(geometry, dict):
729
+ dims = "x".join(
730
+ str(geometry.get(k))
731
+ for k in ("width", "height")
732
+ if geometry.get(k) is not None
733
+ )
734
+ fps = geometry.get("fps")
735
+ reason = geometry.get("reason") or qc.get("output_geometry_reason")
736
+ fps_suffix = f"@{fps}fps" if fps else ""
737
+ reason_suffix = f" reason={reason}" if reason else ""
738
+ parts.append(f"output_geometry={dims or geometry}{fps_suffix}{reason_suffix}")
739
+ warnings = qc.get("warnings")
740
+ if warnings:
741
+ parts.append(
742
+ f"warnings={len(warnings) if isinstance(warnings, list) else warnings}"
743
+ )
744
+ return ["[video-recap] cut QC: " + "; ".join(parts)] if parts else []
745
+
746
+
747
+ def _surface_cut_qc(work_dir):
748
+ plan = _load_json(Path(work_dir) / "clip_plan_validated.json")
749
+ qc = plan.get("qc") if isinstance(plan, dict) else None
750
+ for line in _cut_qc_summary_lines(qc):
751
+ print(line, flush=True)
752
+ blocking = _cut_qc_blocking_reasons(qc)
753
+ if blocking:
754
+ raise SystemExit("video-cut QC blocking/fail status: " + "; ".join(blocking))
755
+ return qc
756
+
757
+
758
+ def _fmt_range(start, end):
759
+ try:
760
+ return f"{float(start):.3f}-{float(end):.3f}s"
761
+ except (TypeError, ValueError):
762
+ return f"{start}-{end}s"
763
+
764
+
765
+ def _material_library_dir(args):
766
+ return (
767
+ args.material_library_dir
768
+ or os.environ.get("VIDEO_RECAP_MATERIAL_LIBRARY_DIR")
769
+ or None
770
+ )
771
+
772
+
773
+ def _materials_enabled(args):
774
+ return bool(_material_library_dir(args) and args.use_materials)
775
+
776
+
777
+ def _save_materials_enabled(args):
778
+ return bool(_material_library_dir(args) and args.save_materials)
779
+
780
+
781
+ def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
782
+ brief = Path(work_dir) / "agent_narration_brief.md"
783
+ print("=" * 50)
784
+ if brief.exists() and "Research the story FIRST" in brief.read_text(
785
+ encoding="utf-8"
786
+ ):
787
+ print(
788
+ "[video-recap] ⚑ 理解素材偏薄:先按 brief 顶部「Research the story FIRST」调研并写 "
789
+ "background_research.json,再写解说,避免看图说话。"
790
+ )
791
+ print(f"[video-recap] ⏸ 阅读 {brief}(按 video-script 规则)后写入 {need_text}")
792
+ if inspect_hint:
793
+ print(f"[video-recap] 先核对状态/时间轴(建议性): {inspect_hint}")
794
+ print(f"[video-recap] 写完后重跑继续: {cont}")
795
+ print("=" * 50)