@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,761 @@
1
+ """Build review prompts and normalize model review responses."""
2
+
3
+ import json
4
+
5
+
6
+ import re
7
+
8
+ from pathlib import Path
9
+
10
+ from lib import stable_hash
11
+
12
+ from evidence_bundle import (
13
+ _clip_text,
14
+ _in_ranges,
15
+ _safe_time,
16
+ build_evidence_bundle,
17
+ build_review_coverage_metadata,
18
+ render_evidence_bundle,
19
+ validate_public_evidence_contract,
20
+ )
21
+ from review_grounding import _load, _source_fingerprint
22
+
23
+ CATEGORIES = [
24
+ "hallucination",
25
+ "weak_hook",
26
+ "no_throughline",
27
+ "narrating_picture",
28
+ "density",
29
+ "pacing",
30
+ "cliche",
31
+ "incomplete",
32
+ "disjoint_handoff",
33
+ "promise_mismatch",
34
+ "low_information_gain",
35
+ "not_write_for_ear",
36
+ "grounding_risk",
37
+ "original_audio_conflict",
38
+ "subtitle_readability",
39
+ "ai_flavor",
40
+ "weak_payoff",
41
+ "style_mismatch",
42
+ "packaging_mismatch",
43
+ "example_entity_leak",
44
+ "other",
45
+ ]
46
+
47
+ SCORECARD_KEYS = [
48
+ "promise_match",
49
+ "hook_3s",
50
+ "first_15s_delivery",
51
+ "spine_clarity",
52
+ "stakes_escalation",
53
+ "information_gain",
54
+ "spoken_language",
55
+ "sentence_brevity",
56
+ "tts_pacing",
57
+ "grounding",
58
+ "original_audio_use",
59
+ "subtitle_readability",
60
+ "ending_payoff",
61
+ "style_consistency",
62
+ "ai_flavor",
63
+ "packaging_consistency",
64
+ ]
65
+
66
+ FACTUAL_CATEGORIES = {"hallucination", "incomplete"}
67
+
68
+ COVERAGE_POLICY_VERSION = "coverage_policy_v1"
69
+
70
+ RUBRIC = """你是中文视频解说的创作复核编辑。依据素材证据和已有创作计划审阅草稿,只指出真实问题,宁缺毋滥:
71
+ 1. 反幻觉(最重要):解说里的人物、动作、因果、关系必须由带标签的 evidence 支撑。画面/对白是 timeline evidence(clock=SOURCE 或 OUTPUT);背景资料/user_context 只能作为 context-only(clock=null)辅助识别/消歧。research-only 不能升级成当前画面强事实;若与 research 一致但画面/对白里看不到,最多 severity=suggestion/category=grounding_risk,不要判 error;只有与全部可得证据矛盾才是 severity=error, category=hallucination,并指出冲突证据。
72
+ 2. 导演意图:若提供 recap_story_plan.json,检查草稿是否兑现 viewer promise、POV、dramatic question、情绪路径和 chosen_hypothesis;不要另起一条更“吸睛”但不属于该计划的故事。偏离主线 → no_throughline;承诺不兑现 → promise_mismatch/weak_payoff。
73
+ 3. change-based beats:每个 beat 应改变知识、权力、目标、关系、情绪或风险。若一段只重复上一段、删除后什么都不损失,可报 low_information_gain/pacing;不要用固定段数或秒数代替判断。
74
+ 4. 钩子:开头要提出正文真实兑现的戏剧问题/利害,不是交代场景,也不是无关的留存话术。弱钩子 → weak_hook。
75
+ 5. 给信息而非念画面:观众看得见动作表情;解说只增加上下文、因果、预期、证据支持的解释或跨越。复述画面 → narrating_picture。
76
+ 6. 视听分工:若提供 visual_audio_board.json,检查 narration_job=none 或 audio_owner=original_dialogue/action_sound/ambience/music/silence 的拍是否被旁白无故覆盖;必须听见的原声被盖住 → original_audio_conflict。沉默和低旁白覆盖本身不是问题。
77
+ 7. 人物与反应:不要用旁白解释掉素材中已经能成立的表演、停顿或反应。当前评审只能检查计划/稿件一致性,不能凭少量帧声称最终剪点一定好坏。
78
+ 8. 密度/节奏:7:3 不是配额。只在旁白没有任务、墙到墙压住原声、碎成一句一停,或无意长空档导致因果断裂时,报 density/pacing。
79
+ 9. 去废词:删空泛形容("危机四伏""震撼人心")→ cliche。
80
+ 10. 完整句子:半句话/未收尾 → incomplete。
81
+ 11. 段落衔接:解说块要为随后的原声留白铺垫,下一块要承接原声刚呈现的变化;若两块各说各的、原声进来接不上 → disjoint_handoff。
82
+ 12. 结尾回收:结尾要兑现开头承诺/主线情绪,不要突然停、只复述最后画面、没有情绪/信息回报;弱回收 → weak_payoff。
83
+ 13. 风格一致性:若提供 style_card.json,把它当作表达意图/语气/节奏边界;不符合意图 → style_mismatch。不要把 style_card 当标题/封面/首句包装计划。
84
+ 14. 包装一致性:只有提供 packaging_plan.json 时才评估标题/封面/首句/卖点承诺与正文兑现;缺失不扣分。不一致 → packaging_mismatch。不要让包装反过来改写故事判断。
85
+ 15. 去AI味:若出现模板化、空泛拔高、过度对仗、机械转折、明显 agent 示例残留,可报 ai_flavor;若出现示例人物/占位实体泄漏(如未替换示例名、模板角色)→ example_entity_leak。“不是 A,而是 B”本身不是错误,只有在先虚构旧判断再制造假洞察、或反复机械使用时才建议改写。deslop_qc.json 是 deterministic local report-only QC,不是 AIGC detector,不自动重写;只能作为证据参考,不能仅凭它判定。
86
+ 另外给一份内容效果 scorecard(1-5,advisory:除事实矛盾/残句外不要据此给 error;缺项可以省略,系统会保留为 null/未评分):promise_match/hook_3s/first_15s_delivery/spine_clarity/stakes_escalation/information_gain/spoken_language/sentence_brevity/tts_pacing/grounding/original_audio_use/subtitle_readability/ending_payoff/style_consistency/ai_flavor/packaging_consistency。`sentence_brevity` 衡量的是句子是否简洁而完整,不是越短越高;连续短句造成串珠式 TTS 应降低分数。ai_flavor 分数含义:5=自然、人味强,1=AI味明显。
87
+ 只返回 JSON(不要额外解释),格式:
88
+ {"verdict":"PASS|REVISE|FAIL","summary":"一两句总体判断","scorecard":{"promise_match":1-5,"hook_3s":1-5,"first_15s_delivery":1-5,"spine_clarity":1-5,"stakes_escalation":1-5,"information_gain":1-5,"spoken_language":1-5,"sentence_brevity":1-5,"tts_pacing":1-5,"grounding":1-5,"original_audio_use":1-5,"subtitle_readability":1-5,"ending_payoff":1-5,"style_consistency":1-5,"ai_flavor":1-5,"packaging_consistency":1-5},"hook_candidates_review":[{"candidate":"首句","type":"suspense|contrast|stakes","score":1-5,"keep":true}],"retention_risk_points":[{"time":"00:28","risk":"为什么可能掉人","fix":"怎么改"}],"highest_return_edits":["最值得改的动作"],"information_gain_notes":[{"segment":0,"label":"motive|relationship|stakes|foreshadowing|payoff|context|visual_restatement","note":"证据/改法"}],"spoken_language_rewrites":[{"segment":0,"original":"原句","rewrite":"口语改写","why":"为什么更适合听"}],"grounding_assertions":[{"segment":0,"assertion":"人物/关系/因果断言","source":"visual|asr|research|user_context|unsupported","risk":"谨慎说明"}],"findings":[{"segment":<草稿段号(从0起)或null表示整体>,"severity":"error|warning|suggestion","category":"<上面类别之一>","issue":"问题","fix":"具体改法"}]}"""
89
+
90
+
91
+ def merge_review_findings(chunks):
92
+ """Merge chunk review findings deterministically, keeping highest severity."""
93
+ severity_rank = {"error": 3, "warning": 2, "suggestion": 1}
94
+ by_key = {}
95
+ for chunk in chunks or []:
96
+ for f in (chunk or {}).get("findings", []) or []:
97
+ if not isinstance(f, dict):
98
+ continue
99
+ key = (f.get("segment"), f.get("category"), f.get("issue"))
100
+ old = by_key.get(key)
101
+ if old is None or severity_rank.get(
102
+ f.get("severity"), 0
103
+ ) > severity_rank.get(old.get("severity"), 0):
104
+ by_key[key] = dict(f)
105
+ return sorted(
106
+ by_key.values(),
107
+ key=lambda f: (
108
+ f.get("segment") is None,
109
+ f.get("segment") if f.get("segment") is not None else 10**9,
110
+ f.get("category") or "",
111
+ f.get("issue") or "",
112
+ ),
113
+ )
114
+
115
+
116
+ def _bundle_fingerprint(bundle):
117
+ try:
118
+ return stable_hash(
119
+ {
120
+ "schema_version": bundle.get("schema_version"),
121
+ "clock": bundle.get("clock"),
122
+ "coverage": bundle.get("coverage"),
123
+ "items": bundle.get("items"),
124
+ "context_items": bundle.get("context_items"),
125
+ }
126
+ )
127
+ except (TypeError, ValueError, RecursionError) as exc:
128
+ if isinstance(bundle, dict):
129
+ bundle.setdefault("metadata", {})["evidence_bundle_fingerprint_warning"] = (
130
+ f"evidence bundle fingerprint unavailable: {type(exc).__name__}"
131
+ )
132
+ return ""
133
+
134
+
135
+ def _bundle_fingerprint_warning(bundle):
136
+ if not isinstance(bundle, dict):
137
+ return "evidence bundle fingerprint unavailable: invalid bundle"
138
+ warning = (bundle.get("metadata") or {}).get("evidence_bundle_fingerprint_warning")
139
+ if warning:
140
+ return warning
141
+ return ""
142
+
143
+
144
+ def _append_warning_once(target, warning):
145
+ if warning and warning not in target:
146
+ target.append(warning)
147
+
148
+
149
+ def _bundle_prompt_size(bundle):
150
+ return len(render_evidence_bundle(bundle))
151
+
152
+
153
+ def _chunk_evidence_bundle(bundle, *, max_items=80, max_chars=12000):
154
+ """Split oversized evidence bundles for actual review calls.
155
+
156
+ Chunks are deterministic and range-oriented: selected coverage ranges remain the
157
+ contract, but each chunk carries only the timeline items whose spans overlap that
158
+ range. Context-only research remains advisory and is repeated in each chunk so the
159
+ judge can still use alias/background hints without upgrading them to timeline facts.
160
+ """
161
+ items = list(bundle.get("items") or [])
162
+ if len(items) <= max_items and _bundle_prompt_size(bundle) <= max_chars:
163
+ one = dict(bundle)
164
+ one["chunk_index"] = 0
165
+ one["chunk_count"] = 1
166
+ fp = _bundle_fingerprint(bundle)
167
+ fp_warning = _bundle_fingerprint_warning(bundle)
168
+ one.setdefault("metadata", {})["evidence_bundle_fingerprint"] = fp
169
+ if fp_warning:
170
+ one["metadata"]["evidence_bundle_fingerprint_warning"] = fp_warning
171
+ one.setdefault("warnings", list(bundle.get("warnings") or []))
172
+ _append_warning_once(one["warnings"], fp_warning)
173
+ return [one]
174
+ ranges = bundle.get("coverage", {}).get("selected_ranges") or []
175
+ chunks = []
176
+ used_ids = set()
177
+ for r in ranges:
178
+ r_items = [
179
+ item
180
+ for item in items
181
+ if _in_ranges(
182
+ _safe_time(item, "start"),
183
+ _safe_time(item, "end", _safe_time(item, "start")),
184
+ [r],
185
+ )
186
+ ]
187
+ if not r_items:
188
+ continue
189
+ for start in range(0, len(r_items), max_items):
190
+ part = r_items[start : start + max_items]
191
+ used_ids.update(id(item) for item in part)
192
+ chunk = dict(bundle)
193
+ chunk["items"] = part
194
+ chunk["coverage"] = dict(bundle.get("coverage") or {})
195
+ chunk["coverage"]["selected_ranges"] = [r]
196
+ chunk["chunk_index"] = len(chunks)
197
+ chunks.append(chunk)
198
+ leftovers = [item for item in items if id(item) not in used_ids]
199
+ for start in range(0, len(leftovers), max_items):
200
+ chunk = dict(bundle)
201
+ chunk["items"] = leftovers[start : start + max_items]
202
+ chunk["coverage"] = dict(bundle.get("coverage") or {})
203
+ chunk["coverage"]["selected_ranges"] = []
204
+ chunk["chunk_index"] = len(chunks)
205
+ chunks.append(chunk)
206
+ if not chunks:
207
+ chunk = dict(bundle)
208
+ chunk["items"] = []
209
+ chunk["chunk_index"] = 0
210
+ chunks = [chunk]
211
+ count = len(chunks)
212
+ fp = _bundle_fingerprint(bundle)
213
+ fp_warning = _bundle_fingerprint_warning(bundle)
214
+ for chunk in chunks:
215
+ chunk["chunk_count"] = count
216
+ chunk.setdefault("metadata", {})["chunked_review"] = count > 1
217
+ chunk["metadata"]["evidence_bundle_fingerprint"] = fp
218
+ if fp_warning:
219
+ chunk["metadata"]["evidence_bundle_fingerprint_warning"] = fp_warning
220
+ chunk.setdefault("warnings", list(bundle.get("warnings") or []))
221
+ _append_warning_once(chunk["warnings"], fp_warning)
222
+ return chunks
223
+
224
+
225
+ def _merge_chunk_reviews(chunk_reviews):
226
+ if not chunk_reviews:
227
+ return parse_review_response("")
228
+ if len(chunk_reviews) == 1:
229
+ return chunk_reviews[0]
230
+ verdict_rank = {"PASS": 0, "OK": 0, "REVISE": 1, "FAIL": 2}
231
+ best = max(chunk_reviews, key=lambda r: verdict_rank.get(r.get("verdict"), 1))
232
+ merged = dict(best)
233
+ merged["findings"] = merge_review_findings(chunk_reviews)
234
+ if any(f.get("severity") == "error" for f in merged["findings"]):
235
+ merged["verdict"] = "FAIL" if best.get("verdict") == "FAIL" else "REVISE"
236
+ else:
237
+ merged["verdict"] = best.get("verdict", "REVISE")
238
+ summaries = [
239
+ str(r.get("summary", "")).strip()
240
+ for r in chunk_reviews
241
+ if str(r.get("summary", "")).strip()
242
+ ]
243
+ merged["summary"] = summaries[0] if summaries else merged.get("summary", "")
244
+ merged["chunked_review"] = {
245
+ "chunk_count": len(chunk_reviews),
246
+ "findings_before_merge": sum(
247
+ len(r.get("findings") or []) for r in chunk_reviews
248
+ ),
249
+ "findings_after_merge": len(merged.get("findings") or []),
250
+ }
251
+ return merged
252
+
253
+
254
+ def _research_guardrail_qc(review, context_items):
255
+ assertions = [
256
+ a for a in (review.get("grounding_assertions") or []) if isinstance(a, dict)
257
+ ]
258
+ research_context_assertions = [
259
+ a
260
+ for a in assertions
261
+ if str(a.get("source", "")).lower() == "research"
262
+ or a.get("support") == "context_only"
263
+ or a.get("clock") is None
264
+ ]
265
+ risk_assertions = [
266
+ a
267
+ for a in research_context_assertions
268
+ if "spoiler" in str(a.get("risk", "")).lower()
269
+ or "research-only" in str(a.get("risk", "")).lower()
270
+ or "current-timeline" in str(a.get("risk", "")).lower()
271
+ ]
272
+ grounding_risk_findings = [
273
+ f
274
+ for f in (review.get("findings") or [])
275
+ if isinstance(f, dict) and f.get("category") == "grounding_risk"
276
+ ]
277
+ return {
278
+ "context_only_assertions": len(context_items or [])
279
+ + len(research_context_assertions),
280
+ "spoiler_risk_assertions": len(risk_assertions) + len(grounding_risk_findings),
281
+ "policy": "research evidence is context_only unless visual/asr-supported",
282
+ }
283
+
284
+
285
+ def _load_review_research_context(work_dir):
286
+ path = Path(work_dir) / "background_research.json"
287
+ if not path.exists():
288
+ return {}
289
+ try:
290
+ data = json.loads(path.read_text(encoding="utf-8"))
291
+ except (ValueError, OSError):
292
+ return {}
293
+ return data if isinstance(data, dict) else {}
294
+
295
+
296
+ def _format_review_research_context(research, limit=1200):
297
+ """Compact background_research.json for the quality reviewer.
298
+
299
+ Background research is rendered as context-only/advisory: it can help aliases and
300
+ disambiguation, but it is not timeline grounding for current visual/ASR facts.
301
+ """
302
+ if not isinstance(research, dict) or not research:
303
+ return ""
304
+ lines = []
305
+ for key, label in (
306
+ ("synopsis", "Synopsis"),
307
+ ("episode_context", "Episode context"),
308
+ ("worldbuilding", "Worldbuilding"),
309
+ ):
310
+ value = _clip_text(research.get(key), 500)
311
+ if value:
312
+ lines.append(f"- {label}: {value}")
313
+
314
+ characters = research.get("characters")
315
+ if isinstance(characters, dict) and characters:
316
+ lines.append("- Characters:")
317
+ for name, desc in list(characters.items())[:12]:
318
+ clean_name = _clip_text(name, 60)
319
+ clean_desc = _clip_text(desc, 160)
320
+ if clean_name:
321
+ lines.append(f" - {clean_name}:{clean_desc}")
322
+
323
+ details = research.get("character_details")
324
+ if isinstance(details, dict) and details:
325
+ lines.append("- Character details:")
326
+ for name, info in list(details.items())[:8]:
327
+ if not isinstance(info, dict):
328
+ continue
329
+ bits = []
330
+ aliases = info.get("aliases")
331
+ if isinstance(aliases, list) and aliases:
332
+ bits.append(
333
+ "别名 "
334
+ + "/".join(
335
+ _clip_text(alias, 40)
336
+ for alias in aliases[:4]
337
+ if _clip_text(alias, 40)
338
+ )
339
+ )
340
+ role = _clip_text(info.get("role"), 80)
341
+ if role:
342
+ bits.append(role)
343
+ rels = info.get("relationships")
344
+ if isinstance(rels, list) and rels:
345
+ bits.append(
346
+ ";".join(
347
+ _clip_text(rel, 80) for rel in rels[:4] if _clip_text(rel, 80)
348
+ )
349
+ )
350
+ clean_name = _clip_text(name, 60)
351
+ if clean_name and bits:
352
+ lines.append(f" - {clean_name}:{';'.join(bits)}")
353
+
354
+ arcs = research.get("plot_arcs")
355
+ if isinstance(arcs, list) and arcs:
356
+ lines.append("- Plot arcs:")
357
+ for arc in arcs[:8]:
358
+ if isinstance(arc, dict):
359
+ name = _clip_text(arc.get("name"), 80)
360
+ desc = _clip_text(arc.get("description"), 180)
361
+ status = _clip_text(arc.get("status"), 40)
362
+ if name or desc:
363
+ tail = f" [{status}]" if status else ""
364
+ lines.append(f" - {name}:{desc}{tail}")
365
+ else:
366
+ val = _clip_text(arc, 180)
367
+ if val:
368
+ lines.append(f" - {val}")
369
+
370
+ text = "\n".join(lines).strip()
371
+ return text[:limit]
372
+
373
+
374
+ def _load_optional_json(work_dir, name):
375
+ if work_dir is None:
376
+ return None
377
+ return _load(Path(work_dir), name)
378
+
379
+
380
+ def _format_json_context(title, value, limit=3000):
381
+ if value is None:
382
+ return f"## {title}\n(无)"
383
+ try:
384
+ text = json.dumps(value, ensure_ascii=False, indent=2)
385
+ except (TypeError, ValueError):
386
+ text = str(value)
387
+ return f"## {title}\n{text[:limit]}"
388
+
389
+
390
+ def _clamp_score(value, default=3):
391
+ try:
392
+ score = int(round(float(value)))
393
+ except (TypeError, ValueError):
394
+ score = default
395
+ return max(1, min(5, score))
396
+
397
+
398
+ def _normalise_scorecard(raw):
399
+ # Keep a stable scorecard schema, but do NOT fabricate a judge-looking score for a dimension
400
+ # the model omitted: those stay None ("未评分") rather than a neutral-looking 3.
401
+ source = raw if isinstance(raw, dict) else {}
402
+ return {
403
+ key: (_clamp_score(source[key]) if key in source else None)
404
+ for key in SCORECARD_KEYS
405
+ }
406
+
407
+
408
+ def _normalise_list_of_dicts(value, allowed_keys):
409
+ out = []
410
+ for item in value or []:
411
+ if isinstance(item, dict):
412
+ out.append({key: item.get(key) for key in allowed_keys if key in item})
413
+ return out
414
+
415
+
416
+ def _normalise_string_list(value, limit=12):
417
+ out = []
418
+ for item in value or []:
419
+ text = str(item).strip()
420
+ if text:
421
+ out.append(text)
422
+ if len(out) >= limit:
423
+ break
424
+ return out
425
+
426
+
427
+ def _format_draft(narration):
428
+ lines = []
429
+ for i, seg in enumerate(narration or []):
430
+ if not isinstance(seg, dict):
431
+ continue
432
+ start = seg.get("start", 0)
433
+ end = seg.get("end", 0)
434
+ text = str(seg.get("narration", "")).strip()
435
+ overlap = seg.get("overlaps_speech")
436
+ tag = "" if overlap is None else (" [盖原声]" if overlap else " [静音槽]")
437
+ lines.append(f"{i}. [{float(start):.1f}-{float(end):.1f}s]{tag} {text}")
438
+ return "\n".join(lines)
439
+
440
+
441
+ def build_review_messages(
442
+ narration,
443
+ vlm_analysis,
444
+ asr_result,
445
+ work_dir=None,
446
+ research_context=None,
447
+ evidence_bundle=None,
448
+ ):
449
+ """Pure: assemble the reviewer chat messages (testable without the API)."""
450
+ draft = _format_draft(narration)
451
+ research_obj = (
452
+ _load_review_research_context(work_dir) if work_dir is not None else {}
453
+ )
454
+ if research_context is None:
455
+ research_context = _format_review_research_context(research_obj)
456
+ bundle = evidence_bundle or build_evidence_bundle(
457
+ vlm_analysis,
458
+ asr_result,
459
+ narration,
460
+ timeline="cut_output"
461
+ if any("source_start" in x for x in (vlm_analysis or []) if isinstance(x, dict))
462
+ else "source",
463
+ research=research_obj,
464
+ )
465
+ evidence_text = render_evidence_bundle(bundle)
466
+ # Optional, agent-authored planning/QC artifacts. Bad JSON returns None via _load, matching
467
+ # existing fail-open optional artifact behavior; these only sharpen advisory scoring.
468
+ packaging = _load_optional_json(work_dir, "packaging_plan.json")
469
+ story_plan = _load_optional_json(work_dir, "recap_story_plan.json")
470
+ av_board = _load_optional_json(work_dir, "visual_audio_board.json")
471
+ style_card = _load_optional_json(work_dir, "style_card.json")
472
+ deslop_qc = _load_optional_json(work_dir, "deslop_qc.json")
473
+ user = (
474
+ f"{RUBRIC}\n\n"
475
+ "## Scorecard 评估提示\n"
476
+ "若 work_dir 提供了 packaging_plan/recap_story_plan/visual_audio_board/style_card/deslop_qc,则结合评估:"
477
+ "packaging_plan 只负责标题/封面/首句/卖点承诺与正文兑现;style_card 只负责表达意图、语气、节奏和禁忌;"
478
+ "recap_story_plan 是导演意图/备选假设/chosen POV/change-based beats 的基线;visual_audio_board 是画面/表演/原声/audio_owner/narration_job/剪辑锚点的基线;deslop_qc 是 deterministic local report-only QC,不是 AIGC detector,也不会自动重写,只能当证据参考。"
479
+ "若未提供,则基于解说本身与画面/对白证据评分,但不得因计划文件缺失给 error。统一评估:hook 是否真实兑现;每段是否产生变化/信息增量而非看图说话;"
480
+ "结尾是否兑现开头承诺/主线情绪;是否写给耳朵听(连续完整思路、自然口语、TTS可呼吸,而非一句一停);人物/关系/因果断言是否有 visual/ASR timeline evidence;research/user_context 仅可作为 context-only 辅助。\n"
481
+ "审美/风格/包装/去AI味项是 advisory:可 REVISE,但除事实矛盾/残句外不要给 error。\n\n"
482
+ f"{_format_json_context('packaging_plan.json(标题/封面/首句/卖点包装承诺,可能为空)', packaging)}\n\n"
483
+ f"{_format_json_context('recap_story_plan.json(主线/beats/original moments,可能为空)', story_plan)}\n\n"
484
+ f"{_format_json_context('visual_audio_board.json(画面/原声/字幕/剪辑锚点,可能为空)', av_board)}\n\n"
485
+ f"{_format_json_context('style_card.json(表达意图/语气/节奏/禁忌,不负责包装,可能为空)', style_card)}\n\n"
486
+ f"{_format_json_context('deslop_qc.json(deterministic report-only QC;非AIGC检测器;不自动重写,可能为空)', deslop_qc)}\n\n"
487
+ f"## 背景资料(context-only/advisory:只辅助识别/消歧/弱背景,不是当前画面强事实)\n"
488
+ f"Guardrail: clock=null/context_only;不得把未来剧情或 research-only 关系/因果升级为当前事实。\n"
489
+ f"{research_context or '(无)'}\n\n"
490
+ f"{evidence_text}\n\n"
491
+ f"## 解说草稿(共 {len([s for s in (narration or []) if isinstance(s, dict)])} 段)\n{draft or '(空)'}\n"
492
+ )
493
+ return [{"role": "user", "content": user}]
494
+
495
+
496
+ def _downgrade_context_assertions(assertions):
497
+ out = []
498
+ for item in assertions or []:
499
+ src = str(item.get("source", "")).strip().lower()
500
+ if src in {"research", "user_context"}:
501
+ item = dict(item)
502
+ item["support"] = "context_only"
503
+ item["clock"] = None
504
+ risk = str(item.get("risk", "")).strip()
505
+ label = "research-only" if src == "research" else "user_context-only"
506
+ item["risk"] = (
507
+ risk + "; " if risk else ""
508
+ ) + f"{label}: advisory/context_only, not a strong current-timeline fact"
509
+ out.append(item)
510
+ return out
511
+
512
+
513
+ def parse_review_response(text):
514
+ """Pure: robustly extract the reviewer JSON; fall back to a REVISE-unknown shell."""
515
+ raw = str(text or "")
516
+ candidate = raw
517
+ fence = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw, re.DOTALL)
518
+ if fence:
519
+ candidate = fence.group(1)
520
+ else:
521
+ first, last = raw.find("{"), raw.rfind("}")
522
+ if first != -1 and last > first:
523
+ candidate = raw[first : last + 1]
524
+ try:
525
+ data = json.loads(candidate)
526
+ except ValueError:
527
+ return {
528
+ "verdict": "REVISE",
529
+ "summary": "评审输出无法解析为 JSON,请人工检查。",
530
+ "findings": [],
531
+ "parse_error": True,
532
+ "raw": raw[:2000],
533
+ }
534
+ verdict = str(data.get("verdict", "REVISE")).upper()
535
+ # PASS/REVISE/FAIL is the new vocabulary; OK is kept as a backward-compatible alias.
536
+ if verdict not in ("PASS", "REVISE", "FAIL", "OK"):
537
+ verdict = "REVISE"
538
+ findings = []
539
+ for f in data.get("findings", []) or []:
540
+ if not isinstance(f, dict):
541
+ continue
542
+ sev = str(f.get("severity", "warning")).lower()
543
+ if sev not in ("error", "warning", "suggestion"):
544
+ sev = "warning"
545
+ cat = str(f.get("category", "other")).lower()
546
+ if cat not in CATEGORIES:
547
+ cat = "other"
548
+ # Only factual defects may gate strict mode (severity=error). Craft findings
549
+ # (weak_hook, narrating_picture, cliche, disjoint_handoff, ...) are advisory:
550
+ # clamp them to at most "warning" so they never block on subjective judgement.
551
+ if cat not in FACTUAL_CATEGORIES and sev == "error":
552
+ sev = "warning"
553
+ findings.append(
554
+ {
555
+ "segment": f.get("segment"),
556
+ "severity": sev,
557
+ "category": cat,
558
+ "issue": str(f.get("issue", "")).strip(),
559
+ "fix": str(f.get("fix", "")).strip(),
560
+ }
561
+ )
562
+ # The scorecard and the lists below are PURE ADVISORY enrichment: they never mutate the
563
+ # judge's verdict (deliberately not the colleague's auto-downgrade) and never gate. The
564
+ # hard pre-TTS gate stays exactly where it was — error findings counted in recap.py.
565
+ return {
566
+ "verdict": verdict,
567
+ "summary": str(data.get("summary", "")).strip(),
568
+ "scorecard": _normalise_scorecard(data.get("scorecard")),
569
+ "hook_candidates_review": _normalise_list_of_dicts(
570
+ data.get("hook_candidates_review"),
571
+ ["candidate", "type", "score", "keep", "reason"],
572
+ ),
573
+ "retention_risk_points": _normalise_list_of_dicts(
574
+ data.get("retention_risk_points"), ["time", "risk", "fix", "evidence"]
575
+ ),
576
+ "highest_return_edits": _normalise_string_list(
577
+ data.get("highest_return_edits")
578
+ ),
579
+ "information_gain_notes": _normalise_list_of_dicts(
580
+ data.get("information_gain_notes"), ["segment", "label", "note", "rewrite"]
581
+ ),
582
+ "spoken_language_rewrites": _normalise_list_of_dicts(
583
+ data.get("spoken_language_rewrites"),
584
+ ["segment", "original", "rewrite", "why"],
585
+ ),
586
+ "grounding_assertions": _downgrade_context_assertions(
587
+ _normalise_list_of_dicts(
588
+ data.get("grounding_assertions"),
589
+ ["segment", "assertion", "source", "risk", "support", "clock"],
590
+ )
591
+ ),
592
+ "findings": findings,
593
+ }
594
+
595
+
596
+ def format_review_md(review):
597
+ order = {"error": 0, "warning": 1, "suggestion": 2}
598
+ findings = sorted(
599
+ review.get("findings", []), key=lambda f: order.get(f["severity"], 3)
600
+ )
601
+ counts = {
602
+ s: sum(1 for f in findings if f["severity"] == s)
603
+ for s in ("error", "warning", "suggestion")
604
+ }
605
+ out = [
606
+ "# Narration review",
607
+ "",
608
+ f"Verdict: **{review.get('verdict', 'REVISE')}** "
609
+ f"(errors {counts['error']}, warnings {counts['warning']}, suggestions {counts['suggestion']})",
610
+ "",
611
+ review.get("summary", "") or "_(no summary)_",
612
+ "",
613
+ "## Scorecard",
614
+ ]
615
+ scorecard = review.get("scorecard") or {}
616
+ if scorecard:
617
+ for key in SCORECARD_KEYS:
618
+ v = scorecard.get(key)
619
+ out.append(f"- {key}: {v}/5" if v is not None else f"- {key}: 未评分")
620
+ else:
621
+ out.append("- (none)")
622
+ out.extend(["", "## Highest-return edits"])
623
+ edits = review.get("highest_return_edits") or []
624
+ out.extend([f"- {edit}" for edit in edits] or ["- (none)"])
625
+ out.extend(["", "## Retention risk points"])
626
+ risks = review.get("retention_risk_points") or []
627
+ if risks:
628
+ for item in risks:
629
+ out.append(
630
+ f"- {item.get('time', '?')}: {item.get('risk', '')} — {item.get('fix', '')}"
631
+ )
632
+ else:
633
+ out.append("- (none)")
634
+ out.extend(["", "## Hook candidates review"])
635
+ hooks = review.get("hook_candidates_review") or []
636
+ if hooks:
637
+ for item in hooks:
638
+ out.append(
639
+ f"- {item.get('type', '?')} {item.get('score', '-')}/5: {item.get('candidate', '')} ({'keep' if item.get('keep') else 'drop'}) {item.get('reason', '')}"
640
+ )
641
+ else:
642
+ out.append("- (none)")
643
+ out.extend(
644
+ ["", "## Information gain / write-for-ear / grounding", "### Information gain"]
645
+ )
646
+ notes = review.get("information_gain_notes") or []
647
+ out.extend(
648
+ [
649
+ f"- 段 {n.get('segment')}: {n.get('label')} — {n.get('note', '')} {n.get('rewrite', '')}"
650
+ for n in notes
651
+ ]
652
+ or ["- (none)"]
653
+ )
654
+ out.append("### Spoken rewrites")
655
+ rewrites = review.get("spoken_language_rewrites") or []
656
+ out.extend(
657
+ [
658
+ f"- 段 {r.get('segment')}: {r.get('original', '')} → {r.get('rewrite', '')}({r.get('why', '')})"
659
+ for r in rewrites
660
+ ]
661
+ or ["- (none)"]
662
+ )
663
+ out.append("### Grounding assertions")
664
+ assertions = review.get("grounding_assertions") or []
665
+ out.extend(
666
+ [
667
+ f"- 段 {a.get('segment')}: {a.get('assertion', '')} [{a.get('source', '')}] {a.get('risk', '')}"
668
+ for a in assertions
669
+ ]
670
+ or ["- (none)"]
671
+ )
672
+ out.extend(["", "## Findings"])
673
+ if not findings:
674
+ out.append("- (none)")
675
+ for f in findings:
676
+ seg = "整体" if f["segment"] is None else f"段 {f['segment']}"
677
+ out.append(f"- **[{f['severity']}/{f['category']}] {seg}** — {f['issue']}")
678
+ if f["fix"]:
679
+ out.append(f" - 改法: {f['fix']}")
680
+ return "\n".join(out) + "\n"
681
+
682
+
683
+ def build_grounding_qc(work_dir, review, bundle, *, timeline="source"):
684
+ """Pure-ish compatibility seam: build grounding QC payload without writing it.
685
+
686
+ It reads optional source fingerprints/QC from work_dir to preserve the existing artifact
687
+ contract, but has no side effects.
688
+ """
689
+ work_dir = Path(work_dir)
690
+ items = bundle.get("items") or []
691
+ context = bundle.get("context_items") or []
692
+ warnings = list(bundle.get("warnings") or []) + list(review.get("warnings") or [])
693
+ verdict = "warn" if warnings else "pass"
694
+ if any(
695
+ f.get("severity") == "error"
696
+ for f in (review.get("findings") or [])
697
+ if isinstance(f, dict)
698
+ ):
699
+ verdict = "fail"
700
+ if any(item.get("clock") not in ("source", "output") for item in items):
701
+ verdict = "warn" if verdict == "pass" else verdict
702
+ coverage_meta = build_review_coverage_metadata(bundle)
703
+ visual_items = [item for item in items if item.get("source") == "visual"]
704
+ asr_items = [item for item in items if item.get("source") == "asr"]
705
+ visual_fp = _source_fingerprint(work_dir, "vlm_analysis.json") or (
706
+ stable_hash(visual_items) if visual_items else ""
707
+ )
708
+ asr_fp = _source_fingerprint(work_dir, "asr_result.json") or (
709
+ stable_hash(asr_items) if asr_items else ""
710
+ )
711
+ return {
712
+ "schema_version": 1,
713
+ "owner": "video-script.review",
714
+ "timeline": timeline,
715
+ "coverage_policy_version": COVERAGE_POLICY_VERSION,
716
+ "source_fingerprints": {
717
+ "vlm": visual_fp,
718
+ "asr": asr_fp,
719
+ "research": _source_fingerprint(work_dir, "background_research.json"),
720
+ "clip_plan": _source_fingerprint(work_dir, "clip_plan_validated.json"),
721
+ },
722
+ "review_coverage": {
723
+ "time_ranges": coverage_meta["time_ranges"],
724
+ "scene_count": coverage_meta["scene_count"],
725
+ "asr_count": coverage_meta["asr_count"],
726
+ "dropped_ranges": coverage_meta["dropped_ranges"],
727
+ },
728
+ "evidence_contract": {
729
+ "source_items": sum(1 for item in items if item.get("clock") == "source"),
730
+ "output_items": sum(1 for item in items if item.get("clock") == "output"),
731
+ "unclocked_items": sum(
732
+ 1 for item in items if item.get("clock") not in ("source", "output")
733
+ ),
734
+ "context_only_items": len(context),
735
+ "validation": validate_public_evidence_contract(bundle),
736
+ },
737
+ "index_inputs": {
738
+ "vlm": bool(visual_items),
739
+ "asr": bool(asr_items),
740
+ "research": bool(_source_fingerprint(work_dir, "background_research.json")),
741
+ },
742
+ "speech_window_qc": _load_optional_json(work_dir, "silence_periods.qc.json")
743
+ or {"coarse_asr_windows": 0, "low_confidence_speech_flags": 0},
744
+ "research_guardrail": _research_guardrail_qc(review, context),
745
+ "warnings": warnings,
746
+ "verdict": verdict,
747
+ }
748
+
749
+
750
+ def write_grounding_qc(work_dir, qc):
751
+ """Compatibility seam: write a prebuilt grounding_qc.json payload."""
752
+ work_dir = Path(work_dir)
753
+ (work_dir / "grounding_qc.json").write_text(
754
+ json.dumps(qc, ensure_ascii=False, indent=2), encoding="utf-8"
755
+ )
756
+ return qc
757
+
758
+
759
+ def _write_grounding_qc(work_dir, review, bundle, *, timeline="source"):
760
+ qc = build_grounding_qc(work_dir, review, bundle, timeline=timeline)
761
+ return write_grounding_qc(work_dir, qc)