@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,550 @@
1
+ #!/usr/bin/env python3
2
+ """video-recap inspect — advisory, read-only orientation over a recap work_dir.
3
+
4
+ Pure stdlib. Reads ONLY the JSON artifacts already in work_dir (no ffmpeg, no frame
5
+ reads, no video probing, no new deps, no cross-skill import). Every missing or malformed
6
+ artifact degrades to a clear human message — never a traceback.
7
+
8
+ Two subcommands:
9
+ state summarize the work_dir: source video + fingerprint, full|cut mode, which stage
10
+ artifacts are present vs missing, which file is the NEXT pause the pipeline waits
11
+ on, stale-manifest risk, and storyboard path(s) if present.
12
+ clip-map read clip_plan_validated.json and map a queried window between the OUTPUT and
13
+ SOURCE timelines using the SAME forward affine map cut.py uses
14
+ (output = clip.output_start + (src - clip.source_start), clamped to the clip),
15
+ reimplemented locally. Flags cross-clip boundaries and cut-out gaps.
16
+
17
+ Output: markdown by default; --json for machine-readable; --compact (default ON) truncates
18
+ long free text — pass --full to keep it.
19
+ """
20
+ import argparse
21
+ import json
22
+ from pathlib import Path
23
+
24
+
25
+ # --- artifact catalog --------------------------------------------------------
26
+ # Stage artifacts probed by `state`. Order = rough pipeline order.
27
+ _UNDERSTANDING_ARTIFACTS = [
28
+ "scenes.json",
29
+ "asr_result.json",
30
+ "silence_periods.json",
31
+ "speech_boundary_anchors.json",
32
+ "vlm_analysis.json",
33
+ "understanding_index.json",
34
+ "agent_narration_brief.md",
35
+ ]
36
+ _CUT_ARTIFACTS = [
37
+ "multi_source_manifest.json",
38
+ "clip_plan.json",
39
+ "clip_plan_validated.json",
40
+ "edited_source.mp4",
41
+ ]
42
+ _SCRIPT_ARTIFACTS = [
43
+ "narration.json",
44
+ "narration_lint.json",
45
+ "narration_review.json",
46
+ "original_subtitles.json",
47
+ ]
48
+ _RENDER_ARTIFACTS = [
49
+ "tts_meta.json",
50
+ "narration_mapped.json",
51
+ "timeline.json",
52
+ "subtitles.srt",
53
+ "subtitles.ass",
54
+ "assembly_manifest.json",
55
+ ]
56
+ _STORYBOARD_ARTIFACTS = [
57
+ "storyboard/source_storyboard.json",
58
+ "storyboard/edited_storyboard.json",
59
+ ]
60
+ # Forward-compat: if a write-side state file ever lands, prefer it as the state source.
61
+ _FORWARD_STATE_FILES = ["manifest.json", "task_state.json"]
62
+
63
+ _COMPACT_TEXT_LIMIT = 80
64
+
65
+
66
+ def _truncate(text, compact):
67
+ text = str(text or "").strip()
68
+ if not compact or len(text) <= _COMPACT_TEXT_LIMIT:
69
+ return text
70
+ return text[: _COMPACT_TEXT_LIMIT - 1] + "…"
71
+
72
+
73
+ def _load_json(path):
74
+ """Return (data, error). error is a human string when the file is missing/malformed."""
75
+ path = Path(path)
76
+ if not path.exists():
77
+ return None, f"{path.name} 不存在"
78
+ try:
79
+ return json.loads(path.read_text(encoding="utf-8")), None
80
+ except ValueError:
81
+ return None, f"{path.name} 不是合法 JSON(可能写坏了)"
82
+ except OSError as exc:
83
+ return None, f"{path.name} 读取失败: {exc}"
84
+
85
+
86
+ def _fmt_seconds(value):
87
+ """mm:ss for a numeric seconds value, or the raw value when not numeric."""
88
+ try:
89
+ total = float(value)
90
+ except (TypeError, ValueError):
91
+ return str(value)
92
+ sign = "-" if total < 0 else ""
93
+ total = abs(total)
94
+ m = int(total // 60)
95
+ s = total % 60
96
+ return f"{sign}{m:02d}:{s:05.2f}"
97
+
98
+
99
+ # --- source video discovery --------------------------------------------------
100
+ def _discover_source(work_dir):
101
+ """Find the source video path + fingerprint by reading recap artifacts, most authoritative
102
+ first. Returns a dict {path, fingerprint, origin} with None values + origin="unknown" when
103
+ nothing records it. Never raises."""
104
+ # 1. recap_run_manifest.json — canonical: written before the first pause with both fields.
105
+ data, _ = _load_json(Path(work_dir) / "recap_run_manifest.json")
106
+ if isinstance(data, dict) and data.get("source_video"):
107
+ return {
108
+ "path": data.get("source_video"),
109
+ "fingerprint": data.get("source_video_fingerprint"),
110
+ "origin": "recap_run_manifest.json",
111
+ }
112
+ # 2. assembly_manifest.json — late stage; carries input_video (+ source_video in cut mode).
113
+ data, _ = _load_json(Path(work_dir) / "assembly_manifest.json")
114
+ if isinstance(data, dict) and (data.get("source_video") or data.get("input_video")):
115
+ return {
116
+ "path": data.get("source_video") or data.get("input_video"),
117
+ "fingerprint": data.get("source_video_fingerprint"),
118
+ "origin": "assembly_manifest.json",
119
+ }
120
+ # 3. edited_source.mp4.meta.json — cut mode; fingerprint only, no path.
121
+ data, _ = _load_json(Path(work_dir) / "edited_source.mp4.meta.json")
122
+ if isinstance(data, dict) and data.get("source_video_fingerprint"):
123
+ return {
124
+ "path": None,
125
+ "fingerprint": data.get("source_video_fingerprint"),
126
+ "origin": "edited_source.mp4.meta.json",
127
+ }
128
+ return {"path": None, "fingerprint": None, "origin": "unknown"}
129
+
130
+
131
+ def _discover_multi_source(work_dir):
132
+ data, _ = _load_json(Path(work_dir) / "multi_source_manifest.json")
133
+ if not isinstance(data, dict) or not isinstance(data.get("sources"), list):
134
+ return None
135
+ sources = []
136
+ for raw in data.get("sources") or []:
137
+ if not isinstance(raw, dict):
138
+ continue
139
+ sources.append({
140
+ "source_id": raw.get("source_id"),
141
+ "source_path": raw.get("source_path"),
142
+ "source_name": raw.get("source_name"),
143
+ "source_video_fingerprint": raw.get("source_video_fingerprint"),
144
+ "source_work_dir": raw.get("source_work_dir"),
145
+ "material_id": raw.get("material_id"),
146
+ })
147
+ return {"schema_version": data.get("schema_version", 1), "sources": sources}
148
+
149
+
150
+ # --- clip plan loading + forward affine map ----------------------------------
151
+ def _clip_entries(plan):
152
+ """Normalize a clip plan (dict-with-clips or bare list) to the list of clip dicts."""
153
+ if isinstance(plan, dict):
154
+ entries = plan.get("clips", [])
155
+ elif isinstance(plan, list):
156
+ entries = plan
157
+ else:
158
+ return None
159
+ return entries if isinstance(entries, list) else None
160
+
161
+
162
+ def _normalize_clips(entries):
163
+ """Coerce raw clip entries to {clip_id, source_start, source_end, output_start, output_end}.
164
+
165
+ Mirrors assemble._output_clip_spans / _build_video_clips field access: source_start/source_end
166
+ fall back to start/end; when output_start/output_end are absent they are derived with the same
167
+ forward cursor cut.py uses (sum of prior kept-clip durations). Bad rows are skipped, not fatal.
168
+ """
169
+ clips, cursor = [], 0.0
170
+ for idx, c in enumerate(entries or []):
171
+ if not isinstance(c, dict):
172
+ continue
173
+ try:
174
+ ss = float(c.get("source_start", c.get("start")))
175
+ se = float(c.get("source_end", c.get("end")))
176
+ except (TypeError, ValueError):
177
+ continue
178
+ if se - ss <= 0:
179
+ continue
180
+ out_s, out_e = c.get("output_start"), c.get("output_end")
181
+ if out_s is None or out_e is None:
182
+ out_s, out_e = cursor, cursor + (se - ss)
183
+ cursor += se - ss
184
+ else:
185
+ try:
186
+ out_s, out_e = float(out_s), float(out_e)
187
+ except (TypeError, ValueError):
188
+ out_s, out_e = cursor, cursor + (se - ss)
189
+ cursor += se - ss
190
+ else:
191
+ cursor = max(cursor, out_e)
192
+ clips.append({
193
+ "clip_id": c.get("clip_id", idx),
194
+ "source_id": c.get("source_id"),
195
+ "source_path": c.get("source_path"),
196
+ "source_start": ss,
197
+ "source_end": se,
198
+ "output_start": out_s,
199
+ "output_end": out_e,
200
+ "reason": str(c.get("reason", c.get("note", ""))).strip(),
201
+ })
202
+ return clips
203
+
204
+
205
+ def _source_to_output(src, clip):
206
+ """The forward affine map cut.py:319 uses, clamped to the clip's output span."""
207
+ mapped = clip["output_start"] + (float(src) - clip["source_start"])
208
+ return round(max(clip["output_start"], min(mapped, clip["output_end"])), 3)
209
+
210
+
211
+ def _output_to_source(out, clip):
212
+ """Inverse of the forward affine map (same slope, clamped to the clip's source span)."""
213
+ mapped = clip["source_start"] + (float(out) - clip["output_start"])
214
+ return round(max(clip["source_start"], min(mapped, clip["source_end"])), 3)
215
+
216
+
217
+ def _overlap(a0, a1, b0, b1):
218
+ """Intersection [lo, hi] of two closed ranges, or None when they do not overlap.
219
+
220
+ A zero-width query (a0 == a1, e.g. --output-start 10 --output-end 10) is treated as a
221
+ POINT lookup: it returns (a0, a0) when the point lies within [b0, b1], so a point that sits
222
+ inside a clip is reported instead of silently falling through to "not in any clip".
223
+ """
224
+ if a0 == a1:
225
+ return (a0, a0) if b0 <= a0 <= b1 else None
226
+ lo, hi = max(a0, b0), min(a1, b1)
227
+ return (lo, hi) if hi > lo else None
228
+
229
+
230
+ # --- state subcommand --------------------------------------------------------
231
+ def _present(work_dir, name):
232
+ return (Path(work_dir) / name).exists()
233
+
234
+
235
+ def _detect_mode(work_dir):
236
+ """cut when any cut artifact is present, else full."""
237
+ if any(_present(work_dir, n) for n in _CUT_ARTIFACTS):
238
+ return "cut"
239
+ return "full"
240
+
241
+
242
+ def _next_pause(work_dir, mode):
243
+ """The artifact the pipeline is currently waiting on the agent to write, or None when the
244
+ next-needed input is already present. Mirrors recap.py's two-pause cut flow / single-pause full
245
+ flow purely from file presence (advisory; the orchestrator owns the real decision)."""
246
+ if mode == "cut":
247
+ if not _present(work_dir, "clip_plan.json"):
248
+ return ("clip_plan.json", "pass1: 写剪辑计划(只写 clip_plan.json)")
249
+ if not _present(work_dir, "narration.json"):
250
+ return ("narration.json", "pass2: 对着剪好的成片用 OUTPUT 时间轴写解说")
251
+ return None
252
+ if not _present(work_dir, "narration.json"):
253
+ return ("narration.json", "写解说 narration.json")
254
+ return None
255
+
256
+
257
+ def _stale_manifest_note(work_dir, mode):
258
+ """Advisory stale-manifest risks read purely from JSON (no fingerprint recompute — that would
259
+ need the source bytes). Surfaces the two desync traps recap.py guards: a cut narration written
260
+ against an older clip_plan, and a missing run manifest."""
261
+ notes = []
262
+ if not _present(work_dir, "recap_run_manifest.json"):
263
+ notes.append("缺少 recap_run_manifest.json:无法证明 work_dir 属于当前视频/参数(resume 会被拒绝)。")
264
+ if mode == "cut" and _present(work_dir, "narration.json"):
265
+ ledger, _ = _load_json(Path(work_dir) / "recap_phase.json")
266
+ if isinstance(ledger, dict):
267
+ recorded = ledger.get("clip_plan_fingerprint")
268
+ if recorded is None:
269
+ notes.append("recap_phase.json 未记录 clip_plan_fingerprint:无法判断 narration 是否对当前剪辑写的。")
270
+ else:
271
+ notes.append("有 narration.json 但缺少 recap_phase.json:无法判断解说是否对当前剪辑写的(可能 stale)。")
272
+ return notes
273
+
274
+
275
+ def _present_storyboards(work_dir):
276
+ return [n for n in _STORYBOARD_ARTIFACTS if _present(work_dir, n)]
277
+
278
+
279
+ def cmd_state(work_dir, compact=None):
280
+ del compact # Retained for callers that used the pre-v0.4 rendering hint.
281
+ work_dir = Path(work_dir)
282
+ if not work_dir.exists():
283
+ return {"error": f"work_dir 不存在: {work_dir}"}
284
+
285
+ forward = [n for n in _FORWARD_STATE_FILES if _present(work_dir, n)]
286
+ mode = _detect_mode(work_dir)
287
+ source = _discover_source(work_dir)
288
+
289
+ groups = {
290
+ "understanding": _UNDERSTANDING_ARTIFACTS,
291
+ "cut": _CUT_ARTIFACTS,
292
+ "script": _SCRIPT_ARTIFACTS,
293
+ "render": _RENDER_ARTIFACTS,
294
+ }
295
+ artifacts = {}
296
+ for group, names in groups.items():
297
+ artifacts[group] = {
298
+ "present": [n for n in names if _present(work_dir, n)],
299
+ "missing": [n for n in names if not _present(work_dir, n)],
300
+ }
301
+
302
+ pause = _next_pause(work_dir, mode)
303
+ return {
304
+ "work_dir": str(work_dir),
305
+ "mode": mode,
306
+ "forward_state_files": forward,
307
+ "source_video": source,
308
+ "multi_source": _discover_multi_source(work_dir),
309
+ "next_pause": (
310
+ {"artifact": pause[0], "hint": pause[1]} if pause else None
311
+ ),
312
+ "artifacts": artifacts,
313
+ "storyboards": _present_storyboards(work_dir),
314
+ "stale_manifest_notes": _stale_manifest_note(work_dir, mode),
315
+ }
316
+
317
+
318
+ def _render_state_md(state, compact):
319
+ if "error" in state:
320
+ return state["error"]
321
+ lines = [f"# recap work_dir 状态: {state['work_dir']}", ""]
322
+ if state["forward_state_files"]:
323
+ lines.append(f"状态来源(write-side manifest): {', '.join(state['forward_state_files'])}")
324
+ lines.append(f"模式: **{state['mode']}**")
325
+ src = state["source_video"]
326
+ path = src["path"] or "unknown"
327
+ fp = src["fingerprint"]
328
+ fp_short = (fp[:12] + "…") if isinstance(fp, str) and len(fp) > 12 else (fp or "unknown")
329
+ lines.append(f"源视频: {_truncate(path, compact)} (fp {fp_short}, 来源 {src['origin']})")
330
+ if state.get("multi_source"):
331
+ lines.append("多源素材:")
332
+ for s in state["multi_source"].get("sources", []):
333
+ fp = s.get("source_video_fingerprint")
334
+ short = (fp[:12] + "…") if isinstance(fp, str) and len(fp) > 12 else (fp or "unknown")
335
+ lines.append(
336
+ f" - {s.get('source_id')}: {_truncate(s.get('source_path'), compact)} "
337
+ f"(fp {short}, work_dir {s.get('source_work_dir')}, material {s.get('material_id')})"
338
+ )
339
+ lines.append("")
340
+
341
+ if state["next_pause"]:
342
+ np = state["next_pause"]
343
+ lines.append(f"下一步暂停 → 等待写入 **{np['artifact']}** ({np['hint']})")
344
+ else:
345
+ lines.append("下一步暂停 → 无(所需输入已就绪,可继续 voiceover/assemble)")
346
+ lines.append("")
347
+
348
+ lines.append("## 各阶段产物")
349
+ for group, label in (("understanding", "理解"), ("cut", "剪辑"),
350
+ ("script", "解说"), ("render", "渲染")):
351
+ info = state["artifacts"][group]
352
+ lines.append(f"### {label}")
353
+ lines.append(f" 有: {', '.join(info['present']) or '(无)'}")
354
+ lines.append(f" 缺: {', '.join(info['missing']) or '(无)'}")
355
+ lines.append("")
356
+
357
+ if state["storyboards"]:
358
+ lines.append("## storyboard")
359
+ for s in state["storyboards"]:
360
+ lines.append(f" - {s}")
361
+ lines.append("")
362
+
363
+ lines.append("## stale-manifest 风险")
364
+ if state["stale_manifest_notes"]:
365
+ for n in state["stale_manifest_notes"]:
366
+ lines.append(f" ⚠️ {n}")
367
+ else:
368
+ lines.append(" 无明显 stale 风险。")
369
+ return "\n".join(lines)
370
+
371
+
372
+ # --- clip-map subcommand -----------------------------------------------------
373
+ def cmd_clip_map(work_dir, output_start, output_end, source_start, source_end, compact):
374
+ work_dir = Path(work_dir)
375
+ validated = work_dir / "clip_plan_validated.json"
376
+ if not validated.exists():
377
+ return {"error": "clip_plan_validated.json 不存在:这不是 cut 运行,或剪辑计划尚未被 cut.py 校验。"
378
+ "(full 模式没有源↔输出映射;cut 模式请先跑 video-cut。)"}
379
+ plan, err = _load_json(validated)
380
+ if err:
381
+ return {"error": err}
382
+ entries = _clip_entries(plan)
383
+ if entries is None:
384
+ return {"error": "clip_plan_validated.json 结构异常:既不是 clips 数组也不是 {\"clips\": [...]}。"}
385
+ clips = _normalize_clips(entries)
386
+ if not clips:
387
+ return {"error": "clip_plan_validated.json 没有有效的 clip(每段需要 source_start/source_end)。"}
388
+
389
+ have_output = output_start is not None or output_end is not None
390
+ have_source = source_start is not None or source_end is not None
391
+ if not have_output and not have_source:
392
+ return {"error": "请用 --output-start/--output-end 或 --source-start/--source-end 指定要查询的窗口。"}
393
+
394
+ result = {
395
+ "work_dir": str(work_dir),
396
+ "clip_count": len(clips),
397
+ "source_span": [clips[0]["source_start"], clips[-1]["source_end"]],
398
+ "output_span": [clips[0]["output_start"], clips[-1]["output_end"]],
399
+ "queries": [],
400
+ }
401
+ if have_output:
402
+ result["queries"].append(
403
+ _map_output_window(output_start, output_end, clips, compact))
404
+ if have_source:
405
+ result["queries"].append(
406
+ _map_source_window(source_start, source_end, clips, compact))
407
+ return result
408
+
409
+
410
+ def _map_output_window(out_start, out_end, clips, compact):
411
+ """Map a queried OUTPUT window to its SOURCE window(s), one per touched clip."""
412
+ out_lo = clips[0]["output_start"] if out_start is None else float(out_start)
413
+ out_hi = clips[-1]["output_end"] if out_end is None else float(out_end)
414
+ if out_hi < out_lo:
415
+ out_lo, out_hi = out_hi, out_lo
416
+ segments = []
417
+ for clip in clips:
418
+ ov = _overlap(out_lo, out_hi, clip["output_start"], clip["output_end"])
419
+ if not ov:
420
+ continue
421
+ segments.append({
422
+ "clip_id": clip["clip_id"],
423
+ "source_id": clip.get("source_id"),
424
+ "source_path": clip.get("source_path"),
425
+ "output": [round(ov[0], 3), round(ov[1], 3)],
426
+ "source": [_output_to_source(ov[0], clip), _output_to_source(ov[1], clip)],
427
+ "reason": _truncate(clip.get("reason"), compact),
428
+ })
429
+ return {
430
+ "direction": "output→source",
431
+ "query": [round(out_lo, 3), round(out_hi, 3)],
432
+ "clips_touched": [s["clip_id"] for s in segments],
433
+ "cross_clip_boundary": len(segments) > 1,
434
+ "segments": segments,
435
+ # An OUTPUT window can't fall outside any clip (output is contiguous), so no cut-out gaps.
436
+ "cut_out_source_gaps": [],
437
+ }
438
+
439
+
440
+ def _map_source_window(src_start, src_end, clips, compact):
441
+ """Map a queried SOURCE window to its OUTPUT window(s), flagging source ranges in no clip."""
442
+ src_lo = clips[0]["source_start"] if src_start is None else float(src_start)
443
+ src_hi = clips[-1]["source_end"] if src_end is None else float(src_end)
444
+ if src_hi < src_lo:
445
+ src_lo, src_hi = src_hi, src_lo
446
+ segments = []
447
+ covered = []
448
+ for clip in clips:
449
+ ov = _overlap(src_lo, src_hi, clip["source_start"], clip["source_end"])
450
+ if not ov:
451
+ continue
452
+ covered.append(ov)
453
+ segments.append({
454
+ "clip_id": clip["clip_id"],
455
+ "source_id": clip.get("source_id"),
456
+ "source_path": clip.get("source_path"),
457
+ "source": [round(ov[0], 3), round(ov[1], 3)],
458
+ "output": [_source_to_output(ov[0], clip), _source_to_output(ov[1], clip)],
459
+ "reason": _truncate(clip.get("reason"), compact),
460
+ })
461
+ # Cut-out gaps: parts of [src_lo, src_hi] covered by NO kept clip (footage that was cut away).
462
+ gaps = []
463
+ cursor = src_lo
464
+ for lo, hi in sorted(covered):
465
+ if lo > cursor:
466
+ gaps.append([round(cursor, 3), round(lo, 3)])
467
+ cursor = max(cursor, hi)
468
+ if cursor < src_hi:
469
+ gaps.append([round(cursor, 3), round(src_hi, 3)])
470
+ return {
471
+ "direction": "source→output",
472
+ "query": [round(src_lo, 3), round(src_hi, 3)],
473
+ "clips_touched": [s["clip_id"] for s in segments],
474
+ "cross_clip_boundary": len(segments) > 1,
475
+ "segments": segments,
476
+ "cut_out_source_gaps": gaps,
477
+ }
478
+
479
+
480
+ def _render_clip_map_md(result, compact):
481
+ if "error" in result:
482
+ return result["error"]
483
+ lines = [f"# clip-map: {result['work_dir']}", ""]
484
+ lines.append(
485
+ f"{result['clip_count']} clips · "
486
+ f"源 {_fmt_seconds(result['source_span'][0])}–{_fmt_seconds(result['source_span'][1])} · "
487
+ f"输出 {_fmt_seconds(result['output_span'][0])}–{_fmt_seconds(result['output_span'][1])}")
488
+ for q in result["queries"]:
489
+ lines.append("")
490
+ lines.append(f"## {q['direction']} 查询 "
491
+ f"{_fmt_seconds(q['query'][0])}–{_fmt_seconds(q['query'][1])}")
492
+ if q["cross_clip_boundary"]:
493
+ lines.append(f" ⚠️ 跨剪辑边界(涉及 clip {q['clips_touched']})")
494
+ if not q["segments"]:
495
+ lines.append(" (该窗口不落在任何保留片段内)")
496
+ for s in q["segments"]:
497
+ src = f"{_fmt_seconds(s['source'][0])}–{_fmt_seconds(s['source'][1])}"
498
+ out = f"{_fmt_seconds(s['output'][0])}–{_fmt_seconds(s['output'][1])}"
499
+ reason = f" 〔{s['reason']}〕" if s.get("reason") else ""
500
+ sid = f" {s.get('source_id')}" if s.get("source_id") else ""
501
+ spath = f" `{_truncate(s.get('source_path'), compact)}`" if s.get("source_path") else ""
502
+ lines.append(f" clip {s['clip_id']}{sid}{spath}: 源 {src} ↔ 输出 {out}{reason}")
503
+ if q["cut_out_source_gaps"]:
504
+ lines.append(" ✂️ 被剪掉的源区间(不在成片里):")
505
+ for g in q["cut_out_source_gaps"]:
506
+ lines.append(f" {_fmt_seconds(g[0])}–{_fmt_seconds(g[1])}")
507
+ return "\n".join(lines)
508
+
509
+
510
+ # --- CLI ---------------------------------------------------------------------
511
+ def main(argv=None):
512
+ parser = argparse.ArgumentParser(
513
+ prog="recap_inspect.py",
514
+ description="Advisory read-only inspection of a video-recap work_dir (pure JSON).")
515
+ parser.add_argument("--work-dir", required=True, help="recap work_dir to inspect")
516
+ parser.add_argument("--json", action="store_true", help="machine-readable JSON output")
517
+ compact = parser.add_mutually_exclusive_group()
518
+ compact.add_argument("--compact", dest="compact", action="store_true", default=True,
519
+ help="truncate long free text to keep agent context small (default ON)")
520
+ compact.add_argument("--full", dest="compact", action="store_false",
521
+ help="do not truncate long free text")
522
+ sub = parser.add_subparsers(dest="command", required=True)
523
+
524
+ sub.add_parser("state", help="summarize work_dir + the next pause the pipeline is waiting on")
525
+
526
+ cm = sub.add_parser("clip-map", help="map a window between OUTPUT and SOURCE timelines")
527
+ cm.add_argument("--output-start", type=float, default=None)
528
+ cm.add_argument("--output-end", type=float, default=None)
529
+ cm.add_argument("--source-start", type=float, default=None)
530
+ cm.add_argument("--source-end", type=float, default=None)
531
+
532
+ args = parser.parse_args(argv)
533
+
534
+ if args.command == "state":
535
+ result = cmd_state(args.work_dir)
536
+ rendered = _render_state_md(result, args.compact)
537
+ else:
538
+ result = cmd_clip_map(args.work_dir, args.output_start, args.output_end,
539
+ args.source_start, args.source_end, args.compact)
540
+ rendered = _render_clip_map_md(result, args.compact)
541
+
542
+ if args.json:
543
+ print(json.dumps(result, ensure_ascii=False, indent=2))
544
+ else:
545
+ print(rendered)
546
+ return 0
547
+
548
+
549
+ if __name__ == "__main__":
550
+ raise SystemExit(main())
@@ -0,0 +1,77 @@
1
+ """Apply the optional or strict narration-review gate before TTS."""
2
+
3
+ from pathlib import Path
4
+
5
+ from recap_runtime import _env_bool, _load_json
6
+
7
+
8
+ def review_narration_enabled(args):
9
+ if getattr(args, "review_narration", None) is not None:
10
+ return bool(args.review_narration)
11
+ return _env_bool("REVIEW_NARRATION", True)
12
+
13
+
14
+ def require_narration_review(args):
15
+ if getattr(args, "require_narration_review", False):
16
+ return True
17
+ return _env_bool("REQUIRE_NARRATION_REVIEW", False)
18
+
19
+
20
+ def review_result_status(work_dir):
21
+ data = _load_json(Path(work_dir) / "narration_review.json")
22
+ if not isinstance(data, dict):
23
+ return {"ok": False, "reason": "missing or invalid narration_review.json"}
24
+ findings = [f for f in (data.get("findings") or []) if isinstance(f, dict)]
25
+ error_count = sum(1 for finding in findings if finding.get("severity") == "error")
26
+ if data.get("parse_error"):
27
+ return {
28
+ "ok": False,
29
+ "reason": "parse_error",
30
+ "review": data,
31
+ "errors": error_count,
32
+ }
33
+ if error_count:
34
+ return {
35
+ "ok": False,
36
+ "reason": f"error {error_count}",
37
+ "review": data,
38
+ "errors": error_count,
39
+ }
40
+ # Strict mode gates only on parse errors or factual error findings. The model's
41
+ # holistic verdict stays advisory because craft-class findings are warnings.
42
+ return {"ok": True, "reason": "ok", "review": data, "errors": error_count}
43
+
44
+
45
+ def clear_narration_review_artifacts(work_dir):
46
+ """Remove prior review artifacts before a fresh pre-TTS review run."""
47
+ for name in ("narration_review.json", "narration_review.md"):
48
+ try:
49
+ (Path(work_dir) / name).unlink()
50
+ except FileNotFoundError:
51
+ pass
52
+
53
+
54
+ def run_narration_review(work_dir, args, *, run, timeline="source"):
55
+ """Run the review gate using the caller's skill-command runner."""
56
+ strict = require_narration_review(args)
57
+ if not review_narration_enabled(args) and not strict:
58
+ return False
59
+ try:
60
+ clear_narration_review_artifacts(work_dir)
61
+ # Pin the grounding timeline so stale cut artifacts cannot alter auto-detection.
62
+ review_args = ["--work-dir", work_dir, "--timeline", timeline]
63
+ if strict and timeline == "cut_output":
64
+ review_args.append("--strict-evidence")
65
+ run("video-script", "review.py", *review_args)
66
+ except SystemExit as exc:
67
+ if strict:
68
+ raise SystemExit(f"严格解说评审失败,已阻止 TTS: {exc}") from exc
69
+ print(f"[video-recap] ⚠️ 建议性评审失败,继续执行 TTS: {exc}", flush=True)
70
+ return False
71
+
72
+ status = review_result_status(work_dir)
73
+ if strict and not status["ok"]:
74
+ raise SystemExit(f"严格解说评审未通过,已阻止 TTS: {status['reason']}")
75
+ if strict:
76
+ print("[video-recap] ✅ 严格解说评审通过,继续 TTS", flush=True)
77
+ return True