@oh-story/dsh 0.1.4 → 0.1.5

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 +2692 -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,678 @@
1
+ """Enforce narration timing, evidence, budget, and sentence-integrity rules."""
2
+
3
+ import importlib.util
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from lib import CONFIG
8
+ from lib import log
9
+ from agent_text import (
10
+ _clean_narration_punctuation,
11
+ _find_scene_for_midpoint,
12
+ _normalise_narration_segment,
13
+ _post_dedup_narration,
14
+ _recommended_char_budget,
15
+ _scene_available_seconds,
16
+ _text_char_count,
17
+ _truncate_at_sentence,
18
+ )
19
+ from speech_ownership import (
20
+ entry_overlaps_source_speech,
21
+ load_source_sentence_evidence,
22
+ )
23
+
24
+ try:
25
+ from deslop_qc import analyze_deslop_qc
26
+ except ModuleNotFoundError:
27
+ _deslop_qc_path = Path(__file__).with_name("deslop_qc.py")
28
+ _deslop_qc_spec = importlib.util.spec_from_file_location(
29
+ "deslop_qc", _deslop_qc_path
30
+ )
31
+ if _deslop_qc_spec is None or _deslop_qc_spec.loader is None:
32
+ raise
33
+ _deslop_qc_module = importlib.util.module_from_spec(_deslop_qc_spec)
34
+ _deslop_qc_spec.loader.exec_module(_deslop_qc_module)
35
+ analyze_deslop_qc = _deslop_qc_module.analyze_deslop_qc
36
+
37
+
38
+ def _lint_issue(level, index, code, message, **extra):
39
+ issue = {"level": level, "index": index, "code": code, "message": message}
40
+ issue.update(extra)
41
+ return issue
42
+
43
+
44
+ def _scene_bounds_for_midpoint(scenes_analysis, start, end):
45
+ scene = _find_scene_for_midpoint(scenes_analysis or [], start, end)
46
+ if not scene:
47
+ return None
48
+ return float(scene.get("start", 0)), float(scene.get("end", 0))
49
+
50
+
51
+ def _frame_fact_times_for_segment(scenes_analysis, start, end):
52
+ """Return frame-fact timestamps covered by a narration segment.
53
+
54
+ These timestamps are cheap visual anchors. A narration slot that spans too
55
+ many anchors is more likely to drift into "general story summary" instead
56
+ of staying attached to what is on screen.
57
+ """
58
+ times = []
59
+ scene = _find_scene_for_midpoint(scenes_analysis or [], start, end)
60
+ if not scene:
61
+ return times
62
+ for raw_ts in (scene.get("frame_facts") or {}).keys():
63
+ try:
64
+ ts = float(raw_ts)
65
+ except (TypeError, ValueError):
66
+ continue
67
+ if float(start) <= ts <= float(end):
68
+ times.append(ts)
69
+ return sorted(times)
70
+
71
+
72
+ def _clip_matches_for_segment(seg, clip_plan):
73
+ if not clip_plan:
74
+ return []
75
+ clips = (
76
+ clip_plan.get("clips", clip_plan) if isinstance(clip_plan, dict) else clip_plan
77
+ )
78
+ if not isinstance(clips, list):
79
+ return []
80
+ try:
81
+ start = float(seg.get("start"))
82
+ end = float(seg.get("end"))
83
+ except (TypeError, ValueError):
84
+ return []
85
+ midpoint = (start + end) / 2
86
+ if seg.get("source_clip_id") is not None:
87
+ try:
88
+ requested = int(seg.get("source_clip_id"))
89
+ except (TypeError, ValueError):
90
+ return []
91
+ return [
92
+ clip
93
+ for clip in clips
94
+ if clip.get("clip_id") == requested
95
+ and float(clip.get("source_start", clip.get("start", 0)))
96
+ <= midpoint
97
+ <= float(clip.get("source_end", clip.get("end", 0)))
98
+ ]
99
+ return [
100
+ clip
101
+ for clip in clips
102
+ if float(clip.get("source_start", clip.get("start", 0)))
103
+ <= midpoint
104
+ <= float(clip.get("source_end", clip.get("end", 0)))
105
+ ]
106
+
107
+
108
+ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
109
+ """Return a blocking issue when narration enters midway through source speech.
110
+
111
+ The validator reports a correction instead of silently moving audio. Source
112
+ sentence integrity is invariant; an editorial policy string cannot bypass it.
113
+ """
114
+ if not speech_owned:
115
+ return None
116
+ # A cold-open at the first frame is not an interruption: the source sentence
117
+ # has not been allowed to start. All later entries must use a measured anchor.
118
+ if start <= 0.25:
119
+ return None
120
+ if not anchors:
121
+ return _lint_issue(
122
+ "error",
123
+ index,
124
+ "source_sentence_anchors_unavailable",
125
+ "Source speech owns this entry but no verified sentence-end anchor is available. "
126
+ "Move/remove the narration or regenerate output speech evidence before TTS.",
127
+ entry_time=round(start, 3),
128
+ suggested_start=None,
129
+ )
130
+ # Only the measured acoustic pause owns a safe entry. A tiny 80ms post-anchor
131
+ # tolerance covers timestamp/sample rounding; the old +450ms allowance could
132
+ # already be several Chinese syllables into the next sentence.
133
+ for anchor in anchors:
134
+ when = anchor["time"]
135
+ try:
136
+ pause_start = float(anchor.get("pause_start", when - 0.12))
137
+ except (TypeError, ValueError):
138
+ pause_start = when - 0.12
139
+ if pause_start - 0.05 <= start <= when + 0.08:
140
+ return None
141
+
142
+ suggested = next(
143
+ (anchor for anchor in anchors if anchor["time"] > start + 0.08), None
144
+ )
145
+ text_tail = str((suggested or {}).get("text_tail") or "").strip()
146
+ return _lint_issue(
147
+ "error",
148
+ index,
149
+ "interrupts_source_sentence",
150
+ "Narration enters before the source sentence finishes. Move this block to the suggested "
151
+ "sentence-end anchor, or shorten/move/remove it when there is no later verified anchor, "
152
+ "then rerun lint before TTS. Source sentence interruption has no override.",
153
+ entry_time=round(start, 3),
154
+ suggested_start=round(float(suggested["time"]), 3) if suggested else None,
155
+ source_text_tail=text_tail,
156
+ anchor_confidence=suggested.get("confidence") if suggested else None,
157
+ )
158
+
159
+
160
+ def lint_narration(
161
+ narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
162
+ ):
163
+ """Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set."""
164
+ errors = []
165
+ warnings = []
166
+ normalized = []
167
+ source_evidence = load_source_sentence_evidence(work_dir, mode=mode)
168
+ source_sentence_anchors = source_evidence["anchors"]
169
+ if not isinstance(narration, list):
170
+ errors.append(
171
+ _lint_issue(
172
+ "error",
173
+ None,
174
+ "invalid_json_shape",
175
+ "narration.json must be a JSON array",
176
+ )
177
+ )
178
+ else:
179
+ for idx, seg in enumerate(narration):
180
+ if not isinstance(seg, dict):
181
+ errors.append(
182
+ _lint_issue(
183
+ "error",
184
+ idx,
185
+ "invalid_segment",
186
+ "Narration segment must be an object",
187
+ )
188
+ )
189
+ continue
190
+ try:
191
+ start = float(seg.get("start"))
192
+ end = float(seg.get("end"))
193
+ except (TypeError, ValueError):
194
+ errors.append(
195
+ _lint_issue(
196
+ "error", idx, "invalid_time", "start/end must be numeric"
197
+ )
198
+ )
199
+ continue
200
+ text = str(seg.get("narration", "")).strip()
201
+ pause_raw = seg.get("pause_after_ms", CONFIG.get("breath_ms", 250))
202
+ try:
203
+ pause = int(pause_raw)
204
+ except (TypeError, ValueError):
205
+ pause = CONFIG.get("breath_ms", 250)
206
+ warnings.append(
207
+ _lint_issue(
208
+ "warning",
209
+ idx,
210
+ "invalid_pause",
211
+ "pause_after_ms is invalid; default will be used",
212
+ )
213
+ )
214
+ if pause < 0:
215
+ warnings.append(
216
+ _lint_issue(
217
+ "warning",
218
+ idx,
219
+ "negative_pause",
220
+ "pause_after_ms is negative; default should be used",
221
+ pause_after_ms=pause,
222
+ )
223
+ )
224
+ if end <= start:
225
+ errors.append(
226
+ _lint_issue(
227
+ "error",
228
+ idx,
229
+ "invalid_time_range",
230
+ "end must be greater than start",
231
+ start=start,
232
+ end=end,
233
+ )
234
+ )
235
+ continue
236
+ if not text:
237
+ errors.append(
238
+ _lint_issue(
239
+ "error",
240
+ idx,
241
+ "empty_narration",
242
+ "narration text must not be empty",
243
+ start=start,
244
+ end=end,
245
+ )
246
+ )
247
+ continue
248
+
249
+ char_count = _text_char_count(text)
250
+ budget = _recommended_char_budget(start, end, pause)
251
+ # estimate at the REAL playback rate (after the narration_speed atempo); otherwise a
252
+ # beat sized to its 1.3x-sped slot looks "over budget" when it actually fits.
253
+ play_rate = max(
254
+ CONFIG.get("speech_rate", 3.5)
255
+ * float(CONFIG.get("narration_speed", 1.0) or 1.0),
256
+ 0.1,
257
+ )
258
+ estimated_tts_seconds = char_count / play_rate
259
+ slot_seconds = _scene_available_seconds(start, end, pause)
260
+ if budget < 5:
261
+ warnings.append(
262
+ _lint_issue(
263
+ "warning",
264
+ idx,
265
+ "slot_too_short",
266
+ "Narration slot is very short; TTS may be clipped",
267
+ start=start,
268
+ end=end,
269
+ budget_chars=budget,
270
+ )
271
+ )
272
+ elif estimated_tts_seconds > slot_seconds:
273
+ warnings.append(
274
+ _lint_issue(
275
+ "warning",
276
+ idx,
277
+ "over_budget",
278
+ "Text may exceed the available TTS slot",
279
+ start=start,
280
+ end=end,
281
+ budget_chars=budget,
282
+ actual_chars=char_count,
283
+ estimated_tts_seconds=round(estimated_tts_seconds, 2),
284
+ slot_seconds=round(slot_seconds, 2),
285
+ )
286
+ )
287
+ if text[-1] not in "。!?!?….":
288
+ warnings.append(
289
+ _lint_issue(
290
+ "warning",
291
+ idx,
292
+ "incomplete_sentence",
293
+ "Narration should end with a complete sentence punctuation",
294
+ text_tail=text[-8:],
295
+ )
296
+ )
297
+
298
+ # A back-to-back narration handoff does not expose the source track, so
299
+ # the next TTS block is not a new source-speech entry. Keep this strict:
300
+ # only authored adjacency (<=150ms) bypasses the sentence anchor gate.
301
+ connected_predecessor = False
302
+ for other_idx, other in enumerate(narration):
303
+ if other_idx == idx or not isinstance(other, dict):
304
+ continue
305
+ try:
306
+ other_start = float(other.get("start"))
307
+ other_end = float(other.get("end"))
308
+ except (TypeError, ValueError):
309
+ continue
310
+ if other_start < start and -0.001 <= start - other_end <= 0.15:
311
+ connected_predecessor = True
312
+ break
313
+ if not connected_predecessor:
314
+ entry_issue = _source_sentence_entry_issue(
315
+ idx,
316
+ start,
317
+ source_sentence_anchors,
318
+ entry_overlaps_source_speech(seg, source_evidence),
319
+ )
320
+ if entry_issue:
321
+ errors.append(entry_issue)
322
+
323
+ scene_bounds = _scene_bounds_for_midpoint(scenes_analysis, start, end)
324
+ if scenes_analysis and not scene_bounds:
325
+ warnings.append(
326
+ _lint_issue(
327
+ "warning",
328
+ idx,
329
+ "outside_scene",
330
+ "Narration midpoint does not match any detected scene",
331
+ start=start,
332
+ end=end,
333
+ )
334
+ )
335
+ elif scene_bounds and (start < scene_bounds[0] or end > scene_bounds[1]):
336
+ warnings.append(
337
+ _lint_issue(
338
+ "warning",
339
+ idx,
340
+ "crosses_scene_boundary",
341
+ "Narration extends outside its midpoint scene boundary",
342
+ start=start,
343
+ end=end,
344
+ scene_start=scene_bounds[0],
345
+ scene_end=scene_bounds[1],
346
+ )
347
+ )
348
+
349
+ frame_fact_times = _frame_fact_times_for_segment(
350
+ scenes_analysis, start, end
351
+ )
352
+ max_visual_seconds = float(
353
+ CONFIG.get("visual_beat_max_seconds", 18.0) or 18.0
354
+ )
355
+ max_visual_facts = int(CONFIG.get("visual_beat_max_facts", 3) or 3)
356
+ if (end - start) > max_visual_seconds and len(
357
+ frame_fact_times
358
+ ) > max_visual_facts:
359
+ warnings.append(
360
+ _lint_issue(
361
+ "warning",
362
+ idx,
363
+ "visual_beat_too_broad",
364
+ "Narration spans many visual anchors; split or tighten timing so the voiceover stays tied to current pictures",
365
+ start=start,
366
+ end=end,
367
+ duration=round(end - start, 2),
368
+ frame_fact_times=[round(ts, 2) for ts in frame_fact_times[:8]],
369
+ )
370
+ )
371
+
372
+ if mode == "cut":
373
+ matches = _clip_matches_for_segment(seg, clip_plan)
374
+ if not matches:
375
+ errors.append(
376
+ _lint_issue(
377
+ "error",
378
+ idx,
379
+ "outside_clip_plan",
380
+ "Cut-mode narration must fall inside a selected clip",
381
+ start=start,
382
+ end=end,
383
+ )
384
+ )
385
+ elif len(matches) > 1 and seg.get("source_clip_id") is None:
386
+ errors.append(
387
+ _lint_issue(
388
+ "error",
389
+ idx,
390
+ "ambiguous_source_clip",
391
+ "Repeated/overlapping clips require source_clip_id",
392
+ start=start,
393
+ end=end,
394
+ )
395
+ )
396
+ if len(matches) == 1:
397
+ clip = matches[0]
398
+ clip_start = float(
399
+ clip.get("source_start", clip.get("start", start))
400
+ )
401
+ clip_end = float(clip.get("source_end", clip.get("end", end)))
402
+ if start < clip_start or end > clip_end:
403
+ warnings.append(
404
+ _lint_issue(
405
+ "warning",
406
+ idx,
407
+ "crosses_clip_boundary",
408
+ "Narration extends beyond its clip; it will be trimmed to the clip and may describe footage that was cut",
409
+ start=start,
410
+ end=end,
411
+ clip_start=round(clip_start, 3),
412
+ clip_end=round(clip_end, 3),
413
+ )
414
+ )
415
+ if seg.get("source_clip_id") is not None:
416
+ try:
417
+ int(seg.get("source_clip_id"))
418
+ except (TypeError, ValueError):
419
+ errors.append(
420
+ _lint_issue(
421
+ "error",
422
+ idx,
423
+ "invalid_source_clip_id",
424
+ "source_clip_id must be an integer",
425
+ )
426
+ )
427
+
428
+ normalized.append(
429
+ {"index": idx, "start": start, "end": end, "char_count": char_count}
430
+ )
431
+
432
+ sorted_segments = sorted(normalized, key=lambda item: item["start"])
433
+ if isinstance(narration, list) and not sorted_segments:
434
+ errors.append(
435
+ _lint_issue(
436
+ "error",
437
+ None,
438
+ "empty_narration_file",
439
+ "narration.json must contain at least one valid narration segment",
440
+ )
441
+ )
442
+ for prev, curr in zip(sorted_segments, sorted_segments[1:]):
443
+ if curr["start"] < prev["end"]:
444
+ errors.append(
445
+ _lint_issue(
446
+ "error",
447
+ curr["index"],
448
+ "time_overlap",
449
+ "Segment overlaps the previous narration segment",
450
+ previous_index=prev["index"],
451
+ previous_end=prev["end"],
452
+ start=curr["start"],
453
+ end=curr["end"],
454
+ )
455
+ )
456
+
457
+ # Block-coverage check (full mode only; cut-mode density is measured on the mapped output
458
+ # timeline, not the source timestamps used here). Coverage is diagnostic, not a creative quota:
459
+ # flag wall-to-wall or sparse drafts so the Agent consciously checks the visual/audio board,
460
+ # plus missing original-audio gaps and fragmented one-sentence TTS blocks.
461
+ metrics = {}
462
+ if mode == "full" and len(sorted_segments) >= 2:
463
+ timeline_start = 0.0
464
+ timeline_end = sorted_segments[-1]["end"]
465
+ scene_ends = []
466
+ for scene in scenes_analysis or []:
467
+ try:
468
+ scene_ends.append(float(scene.get("end")))
469
+ except (AttributeError, TypeError, ValueError):
470
+ continue
471
+ if scene_ends:
472
+ timeline_end = max(timeline_end, max(scene_ends))
473
+ span = max(0.0, timeline_end - timeline_start)
474
+ # Score coverage at the SAME conservative rate the writer is budgeted at
475
+ # (_recommended_char_budget uses speech_rate * speech_safety_margin * narration_speed),
476
+ # else the metric scores ~18% stricter than its own budget and false-flags under_narrated.
477
+ play_rate = max(
478
+ CONFIG["speech_rate"]
479
+ * float(CONFIG.get("speech_safety_margin", 0.85) or 0.85)
480
+ * float(CONFIG.get("narration_speed", 1.0) or 1.0),
481
+ 0.1,
482
+ )
483
+ spoken = [s["char_count"] / play_rate for s in sorted_segments]
484
+ spoken_ends = [
485
+ min(sorted_segments[i]["end"], sorted_segments[i]["start"] + spoken[i])
486
+ for i in range(len(sorted_segments))
487
+ ]
488
+ # Measure the UNION of estimated playback intervals. Adjacent authored
489
+ # blocks can overlap the conservative text-duration estimate; summing them
490
+ # double-counted time and falsely called a recap wall-to-wall.
491
+ estimated_intervals = sorted(
492
+ (sorted_segments[i]["start"], spoken_ends[i])
493
+ for i in range(len(sorted_segments))
494
+ if spoken_ends[i] > sorted_segments[i]["start"]
495
+ )
496
+ merged_intervals = []
497
+ for start, end in estimated_intervals:
498
+ if merged_intervals and start <= merged_intervals[-1][1]:
499
+ merged_intervals[-1][1] = max(merged_intervals[-1][1], end)
500
+ else:
501
+ merged_intervals.append([start, end])
502
+ narrated_seconds = sum(end - start for start, end in merged_intervals)
503
+ coverage = narrated_seconds / span if span > 0 else 0.0
504
+ orig_min = CONFIG.get("original_block_min_seconds", 2.5)
505
+ orig_gaps = [sorted_segments[0]["start"] - timeline_start]
506
+ orig_gaps.extend(
507
+ sorted_segments[i + 1]["start"] - spoken_ends[i]
508
+ for i in range(len(sorted_segments) - 1)
509
+ )
510
+ orig_gaps.append(timeline_end - spoken_ends[-1])
511
+ original_blocks = sum(1 for g in orig_gaps if g >= orig_min)
512
+ avg_chars = sum(s["char_count"] for s in sorted_segments) / len(sorted_segments)
513
+ cov_target = CONFIG.get("narration_coverage_target", 0.7)
514
+ cov_max = CONFIG.get("narration_coverage_max", 0.85)
515
+ cov_min = CONFIG.get("narration_coverage_min", 0.5)
516
+ block_min_chars = CONFIG.get("narration_block_min_chars", 16)
517
+ metrics = {
518
+ "segment_count": len(sorted_segments),
519
+ "timeline_span_seconds": round(span, 2),
520
+ "narrated_seconds": round(narrated_seconds, 2),
521
+ "narration_coverage": round(coverage, 2),
522
+ "coverage_target": cov_target,
523
+ "original_block_count": original_blocks,
524
+ "avg_block_chars": round(avg_chars, 1),
525
+ }
526
+ if coverage > cov_max:
527
+ warnings.append(
528
+ _lint_issue(
529
+ "warning",
530
+ None,
531
+ "no_original_blocks",
532
+ "Narration is nearly wall-to-wall — the original audio never gets to breathe. Pull back at a "
533
+ "few strong moments and write NO narration there so the original plays at full volume. "
534
+ "Choose those moments by audio_owner, not by a fixed ratio.",
535
+ narration_coverage=round(coverage, 2),
536
+ coverage_max=cov_max,
537
+ )
538
+ )
539
+ elif coverage < cov_min:
540
+ warnings.append(
541
+ _lint_issue(
542
+ "warning",
543
+ None,
544
+ "under_narrated",
545
+ "Narration coverage is sparse. This is not automatically wrong: verify that every long gap is "
546
+ "intentionally owned by original dialogue/action/ambience/music/silence in visual_audio_board.json. "
547
+ "Only add a block when it has a specific narration job.",
548
+ narration_coverage=round(coverage, 2),
549
+ coverage_min=cov_min,
550
+ )
551
+ )
552
+ if original_blocks == 0 and span >= 3 * orig_min:
553
+ warnings.append(
554
+ _lint_issue(
555
+ "warning",
556
+ None,
557
+ "no_original_breaks",
558
+ "No deliberate original-audio blocks — narration runs end-to-end with no gap for a strong "
559
+ "original moment (a key line, an action beat, the music). Leave a few multi-second gaps "
560
+ "between blocks where the original plays alone.",
561
+ original_block_min_seconds=orig_min,
562
+ )
563
+ )
564
+ if len(sorted_segments) >= 8 and avg_chars < block_min_chars:
565
+ warnings.append(
566
+ _lint_issue(
567
+ "warning",
568
+ None,
569
+ "fragmented_beats",
570
+ "Beats are fragmented into single short sentences; each is synthesized as a separate TTS "
571
+ "utterance, which sounds choppy. Merge adjacent related sentences into one fluent BLOCK "
572
+ "that completes a continuous thought; sentence count is not the target.",
573
+ avg_block_chars=round(avg_chars, 1),
574
+ block_min_chars=block_min_chars,
575
+ )
576
+ )
577
+
578
+ deslop_qc = analyze_deslop_qc(narration, work_dir=work_dir)
579
+ for blocker in deslop_qc.get("blockers", []):
580
+ errors.append(
581
+ _lint_issue(
582
+ "error",
583
+ blocker.get("index"),
584
+ blocker.get("code", "deslop_qc_blocker"),
585
+ blocker.get("message", "deslop QC objective blocker"),
586
+ source=blocker.get("source"),
587
+ matches=blocker.get("matches"),
588
+ sentence=blocker.get("sentence"),
589
+ )
590
+ )
591
+
592
+ report = {
593
+ "ok": not errors,
594
+ "error_count": len(errors),
595
+ "warning_count": len(warnings),
596
+ "metrics": metrics,
597
+ "deslop_qc": deslop_qc,
598
+ "errors": errors,
599
+ "warnings": warnings,
600
+ }
601
+ if work_dir is not None:
602
+ Path(work_dir, "deslop_qc.json").write_text(
603
+ json.dumps(deslop_qc, ensure_ascii=False, indent=2), encoding="utf-8"
604
+ )
605
+ Path(work_dir, "narration_lint.json").write_text(
606
+ json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
607
+ )
608
+ return report
609
+
610
+
611
+ def validate_narration_or_raise(
612
+ narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
613
+ ):
614
+ report = lint_narration(
615
+ narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir
616
+ )
617
+ if report["errors"]:
618
+ sample = "; ".join(
619
+ f"#{e.get('index')}: {e['code']}" for e in report["errors"][:3]
620
+ )
621
+ raise ValueError(f"narration.json 预检失败: {sample}; 详见 narration_lint.json")
622
+ if report["warnings"]:
623
+ log(
624
+ f"narration lint: {len(report['warnings'])} warnings (see narration_lint.json)"
625
+ )
626
+ else:
627
+ log("narration lint: ok")
628
+ return report
629
+
630
+
631
+ def _validate_narration_budget(narration, scenes_analysis):
632
+ """Validate agent-written narration against timing budgets; trim impossible text safely."""
633
+ if not isinstance(narration, list):
634
+ raise ValueError("narration.json 必须是 JSON 数组")
635
+
636
+ cleaned = []
637
+ for raw in narration:
638
+ item = _normalise_narration_segment(raw, scenes_analysis)
639
+ if not item:
640
+ continue
641
+ max_chars = _recommended_char_budget(
642
+ item["start"], item["end"], item.get("pause_after_ms")
643
+ )
644
+ if max_chars < 5:
645
+ log(f" 丢弃过短解说段 {item['start']:.1f}-{item['end']:.1f}s")
646
+ continue
647
+ if _text_char_count(item["narration"]) > max_chars * 1.25:
648
+ truncated = _truncate_at_sentence(item["narration"], max_chars)
649
+ if truncated and _text_char_count(truncated) >= 5:
650
+ log(f" 解说超预算,已截短: {item['start']:.1f}-{item['end']:.1f}s")
651
+ item["narration"] = truncated
652
+ else:
653
+ log(
654
+ f" 解说超预算且无法安全截断,已丢弃: {item['start']:.1f}-{item['end']:.1f}s"
655
+ )
656
+ continue
657
+ item["narration"] = _clean_narration_punctuation(item["narration"])
658
+ stripped = item["narration"].strip()
659
+ if stripped and stripped[-1] in ",:、;,—":
660
+ item["narration"] = stripped.rstrip(",:、;,—") + "。"
661
+ cleaned.append(item)
662
+
663
+ cleaned.sort(key=lambda n: n["start"])
664
+ deduped = []
665
+ for item in cleaned:
666
+ if deduped and item["start"] < deduped[-1]["end"]:
667
+ prev = deduped[-1]
668
+ log(
669
+ f" 解说时间重叠: {item['start']:.1f}-{item['end']:.1f}s vs "
670
+ f"{prev['start']:.1f}-{prev['end']:.1f}s"
671
+ )
672
+ if _text_char_count(item["narration"]) > _text_char_count(
673
+ prev["narration"]
674
+ ):
675
+ deduped[-1] = item
676
+ else:
677
+ deduped.append(item)
678
+ return _post_dedup_narration(deduped)