@oh-story/dsh 0.1.10 → 0.1.11

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 (444) hide show
  1. package/README.md +7 -7
  2. package/lib/client.js +7 -3
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +212 -142
  5. package/lib/drama/skills/short-drama/SKILL.md +14 -2
  6. package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
  8. package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
  9. package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
  10. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
  11. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
  12. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
  13. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  14. package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
  15. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
  16. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
  17. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
  18. package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
  19. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
  20. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
  21. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
  22. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
  23. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
  24. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
  25. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
  26. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
  27. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
  28. package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
  29. package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
  30. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
  31. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
  32. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
  33. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
  34. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
  35. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
  36. package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
  37. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
  38. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
  39. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
  40. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
  41. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
  42. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
  43. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
  44. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
  45. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
  46. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
  47. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
  48. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
  49. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
  50. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
  51. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
  52. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
  53. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
  54. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
  55. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
  56. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
  57. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
  58. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
  59. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
  60. package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
  61. package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
  62. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
  63. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
  64. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
  65. package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
  66. package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
  67. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
  68. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
  69. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
  70. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
  71. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
  72. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
  73. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
  74. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
  75. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
  76. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
  77. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
  78. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
  79. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
  80. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  81. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
  82. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
  83. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
  84. package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
  85. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
  86. package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
  87. package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
  88. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
  89. package/lib/index.js +71 -21
  90. package/lib/index.js.map +3 -3
  91. package/lib/oh-story/manifest.json +545 -310
  92. package/lib/oh-story/roles/chapter-extractor.md +11 -11
  93. package/lib/oh-story/roles/character-designer.md +1 -2
  94. package/lib/oh-story/roles/consistency-checker.md +11 -9
  95. package/lib/oh-story/roles/narrative-writer.md +24 -25
  96. package/lib/oh-story/roles/story-architect.md +9 -61
  97. package/lib/oh-story/roles/story-explorer.md +20 -125
  98. package/lib/oh-story/roles/story-researcher.md +0 -1
  99. package/lib/oh-story/skills/story/SKILL.md +6 -6
  100. package/lib/oh-story/skills/story/VERSION +1 -1
  101. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
  102. package/lib/oh-story/skills/story/references/author-memory.md +40 -118
  103. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
  104. package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
  105. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
  106. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
  107. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  108. package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
  109. package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
  110. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
  111. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
  112. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
  113. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
  114. package/lib/oh-story/skills/story-import/SKILL.md +63 -579
  115. package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
  116. package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
  117. package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
  118. package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
  119. package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
  120. package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
  121. package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
  122. package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
  123. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
  124. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
  125. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
  126. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
  127. package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
  128. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
  129. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
  130. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
  131. package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
  132. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
  133. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
  134. package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
  135. package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
  136. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
  137. package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  138. package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  139. package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  140. package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  141. package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
  142. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
  143. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
  144. package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  145. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
  146. package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
  147. package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  148. package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
  149. package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  150. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
  151. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
  152. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
  153. package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
  154. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
  155. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
  156. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
  157. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  158. package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
  159. package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
  160. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
  161. package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
  162. package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
  163. package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
  164. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
  165. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  166. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
  167. package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
  168. package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
  169. package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
  170. package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
  171. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
  172. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
  173. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
  174. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
  175. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
  176. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
  177. package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
  178. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
  179. package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
  180. package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
  181. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
  182. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  183. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
  184. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
  185. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
  186. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
  187. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
  188. package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
  189. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
  190. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
  191. package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
  192. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
  193. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
  194. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
  195. package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  196. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
  197. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
  198. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
  199. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
  200. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
  201. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
  202. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
  203. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
  204. package/lib/oh-story/skills/story-review/SKILL.md +19 -273
  205. package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
  206. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
  207. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
  208. package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
  209. package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
  210. package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
  211. package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
  212. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
  213. package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
  214. package/lib/oh-story/skills/story-review/references/solo.md +52 -0
  215. package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
  216. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
  217. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
  218. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
  219. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
  220. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
  221. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
  222. package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
  223. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
  224. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
  225. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
  226. package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  227. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
  228. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
  229. package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  230. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
  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/long-genre-catalog.md +1 -1
  234. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
  236. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
  237. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
  238. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
  239. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
  240. package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
  241. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
  242. package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
  243. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
  244. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  245. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
  246. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
  247. package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
  248. package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
  249. package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
  250. package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
  251. package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
  252. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
  253. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
  254. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
  255. package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
  257. package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
  258. package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
  259. package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  260. package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
  261. package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
  262. package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
  263. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
  264. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  265. package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
  266. package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
  267. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
  268. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
  270. package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
  271. package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
  272. package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
  273. package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
  274. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
  276. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
  277. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
  278. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
  279. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
  280. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
  281. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
  282. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
  283. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
  284. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
  285. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
  286. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
  287. package/lib/types/client/video-studio.d.ts.map +1 -1
  288. package/lib/types/native-hooks.d.ts +2 -2
  289. package/lib/types/native-hooks.d.ts.map +1 -1
  290. package/lib/types/role-tool.d.ts.map +1 -1
  291. package/lib/types/skill-provider.d.ts.map +1 -1
  292. package/lib/types/video-project.d.ts.map +1 -1
  293. package/lib/types/workspace-route.d.ts.map +1 -1
  294. package/lib/video-recap/CHANGELOG.md +60 -1
  295. package/lib/video-recap/README.en.md +348 -137
  296. package/lib/video-recap/README.md +340 -136
  297. package/lib/video-recap/manifest.json +406 -251
  298. package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
  299. package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
  300. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  301. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
  302. package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
  303. package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
  304. package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
  305. package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
  306. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
  307. package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  308. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  309. package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  310. package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  311. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  312. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  313. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
  314. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
  315. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
  316. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
  317. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
  318. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
  319. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
  320. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
  321. package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  322. package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
  323. package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
  324. package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
  325. package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
  326. package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
  327. package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
  328. package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
  329. package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
  330. package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
  331. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
  332. package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
  333. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
  334. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
  335. package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
  336. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
  337. package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  338. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
  339. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
  340. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
  341. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  342. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
  343. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
  344. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
  345. package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
  346. package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
  347. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
  348. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
  349. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
  350. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
  351. package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
  352. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
  353. package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
  354. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
  355. package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
  356. package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
  357. package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
  358. package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
  359. package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
  360. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
  361. package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
  362. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
  363. package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
  364. package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
  365. package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
  366. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
  367. package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
  368. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
  369. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
  370. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
  371. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
  372. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
  373. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
  374. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
  375. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
  376. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
  377. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
  378. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
  379. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
  380. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
  381. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
  382. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
  383. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
  384. package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
  385. package/lib/video-recap/skills/video-script/SKILL.md +16 -13
  386. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
  387. package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
  388. package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
  389. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
  390. package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
  391. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
  392. package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
  393. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
  394. package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
  395. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
  396. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
  397. package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
  398. package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
  399. package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
  400. package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
  401. package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
  402. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  403. package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
  404. package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  405. package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
  406. package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
  407. package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
  408. package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
  409. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
  410. package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
  411. package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
  412. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
  413. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
  414. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
  415. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
  416. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
  417. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
  418. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
  419. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
  420. package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
  421. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
  422. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  423. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
  424. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
  425. package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  426. package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  427. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
  428. package/package.json +1 -1
  429. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
  430. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
  431. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
  432. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
  433. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
  434. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
  435. package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
  436. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
  437. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
  438. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
  439. package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
  440. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
  441. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
  442. /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
  443. /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
  444. /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
@@ -7,6 +7,7 @@ from lib import CONFIG, log
7
7
  from media import _build_video_clips
8
8
  from source_subtitles import _combined_subtitle_entries
9
9
  from timeline import build_timeline, save_timeline
10
+ import packaging
10
11
 
11
12
  def _timeline_subtitle_segments(tts_segments, work_dir, duration_s):
12
13
  """Display-ready subtitle cues for timeline/export text tracks.
@@ -25,10 +26,32 @@ def _timeline_subtitle_segments(tts_segments, work_dir, duration_s):
25
26
  ]
26
27
 
27
28
 
28
- def _emit_timeline(input_video, tts_segments, work_dir, duration_s, canvas, has_bgm):
29
+ def _emit_timeline(input_video, tts_segments, work_dir, duration_s, canvas, has_bgm, *,
30
+ audio_mode="narration", selected_audio_stream=0,
31
+ explicit_audio_mix=None):
29
32
  """Build and persist the backend-neutral multi-track timeline.json."""
30
- video_clips = _build_video_clips(input_video, work_dir, duration_s)
33
+ if audio_mode == "narration" and explicit_audio_mix is None:
34
+ video_clips = _build_video_clips(input_video, work_dir, duration_s)
35
+ else:
36
+ # Non-narration sound comes from the actual current picture input as one
37
+ # complete interval. Re-expanding an old cut plan would substitute different
38
+ # source sound and make the optional editor project misrepresent the render.
39
+ video_clips = [{
40
+ "source_path": str(Path(input_video)),
41
+ "source_start": 0.0,
42
+ "source_end": float(duration_s),
43
+ "timeline_start": 0.0,
44
+ "timeline_end": float(duration_s),
45
+ }]
46
+ # Mix segments are 1:1 with the UNFILTERED narration list, so they must be looked
47
+ # up by their own index; a skipped beat would otherwise shift every later gain and
48
+ # sample bound onto the wrong segment.
49
+ mix_by_index = (
50
+ {item["index"]: item for item in explicit_audio_mix["segments"]}
51
+ if explicit_audio_mix is not None else {}
52
+ )
31
53
  narration_segments = []
54
+ placed_indices = []
32
55
  for seg in tts_segments:
33
56
  s, e = _seg_place_window(seg)
34
57
  if e <= s:
@@ -40,16 +63,20 @@ def _emit_timeline(input_video, tts_segments, work_dir, duration_s, canvas, has_
40
63
  "source_path": seg["placed_audio_path"],
41
64
  "timeline_start": s, "timeline_end": e,
42
65
  "text": seg["narration"],
43
- "overlaps_speech": seg.get("overlaps_speech", True),
44
- "gain": 1.0,
66
+ "overlaps_speech": seg["overlaps_speech"],
67
+ "gain": (
68
+ mix_by_index[seg["index"]]["gain"]
69
+ if explicit_audio_mix is not None else 1.0
70
+ ),
45
71
  }
46
72
  for key in ("source_duck_end", "source_restore_at", "source_handoff_status", "source_entry_status"):
47
73
  if key in seg:
48
74
  narration_item[key] = seg[key]
49
75
  narration_segments.append(narration_item)
76
+ placed_indices.append(seg["index"])
50
77
  fade = CONFIG["duck_fade_seconds"]
51
78
  bgm = None
52
- if has_bgm:
79
+ if has_bgm and explicit_audio_mix is None:
53
80
  bgm = {"source_path": CONFIG["bgm_path"],
54
81
  "volume": CONFIG["bgm_volume"],
55
82
  "ducking_volume": CONFIG["bgm_ducking_volume"],
@@ -57,7 +84,8 @@ def _emit_timeline(input_video, tts_segments, work_dir, duration_s, canvas, has_
57
84
  # carry ducking automation whenever ducking is on at all; even under sidechain
58
85
  # mode the draft gets editable volume keyframes (ffmpeg stays the canonical mix)
59
86
  ducking = None
60
- if CONFIG["ducking_mode"] != "none":
87
+ if audio_mode == "narration" and explicit_audio_mix is None \
88
+ and CONFIG["ducking_mode"] != "none":
61
89
  ducking = {"idle": CONFIG["idle_orig_volume"],
62
90
  "speech": CONFIG["speech_ducking_volume"],
63
91
  "quiet": CONFIG["zone_ducking_volume"],
@@ -66,7 +94,63 @@ def _emit_timeline(input_video, tts_segments, work_dir, duration_s, canvas, has_
66
94
  subtitle_segments = _timeline_subtitle_segments(tts_segments, work_dir, duration_s)
67
95
  timeline = build_timeline(canvas, duration_s, video_clips,
68
96
  narration_segments, bgm=bgm, ducking=ducking,
69
- subtitle_segments=subtitle_segments)
97
+ subtitle_segments=subtitle_segments,
98
+ image_segments=packaging.timeline_image_segments(
99
+ packaging.load_packaging_layers(work_dir, canvas),
100
+ canvas, duration_s))
101
+ if explicit_audio_mix is not None:
102
+ for clip in timeline["tracks"][0]["clips"]:
103
+ clip["audio"] = {
104
+ "role": "picture_audio_not_consumed", "base_gain": 0.0,
105
+ "volume_keyframes": [],
106
+ }
107
+ narration_track = next(
108
+ (track for track in timeline["tracks"] if track.get("name") == "narration"), None
109
+ )
110
+ if narration_track:
111
+ for segment, index in zip(narration_track["segments"], placed_indices):
112
+ adopted = mix_by_index[index]
113
+ segment.update({
114
+ "gain": adopted["gain"],
115
+ "output_start_sample": adopted["output_start_sample"],
116
+ "output_end_sample": adopted["output_end_sample"],
117
+ "sample_rate": 48_000,
118
+ })
119
+ timeline["tracks"].append({
120
+ "kind": "audio", "name": "prepared_bed", "role": "prepared_bed",
121
+ "segments": [{
122
+ "source_path": explicit_audio_mix["prepared"]["prepared_bed.wav"]["path"],
123
+ "timeline_start": 0.0, "timeline_end": float(duration_s), "gain": 1.0,
124
+ }],
125
+ })
126
+ timeline["audio_delivery"] = {
127
+ "mode": "explicit_adopted_full_sound", "sample_rate": 48_000,
128
+ "total_samples": explicit_audio_mix["format"]["total_samples"],
129
+ "master_gain_db": explicit_audio_mix["master_gain_db"],
130
+ "canonical_renderer": "ffmpeg_explicit_mix",
131
+ "reconstructable": False,
132
+ "reconstructable_reason": (
133
+ "optional editor export does not implement the adopted 48 kHz "
134
+ "prepared-bed, per-segment gain, and fixed-master chain"
135
+ ),
136
+ }
137
+ if audio_mode != "narration":
138
+ source_gain = 1.0 if audio_mode == "adopted-packet-copy" else CONFIG["idle_orig_volume"]
139
+ for clip in timeline["tracks"][0]["clips"]:
140
+ clip["audio"].update({
141
+ "base_gain": round(float(source_gain), 4),
142
+ "selected_stream": selected_audio_stream,
143
+ "mode": audio_mode,
144
+ })
145
+ timeline["audio_delivery"] = {
146
+ "mode": audio_mode,
147
+ "selected_stream": selected_audio_stream,
148
+ "packet_frozen": audio_mode == "adopted-packet-copy",
149
+ "reconstructable": (
150
+ audio_mode == "adopted-packet-copy" and selected_audio_stream == 0
151
+ ),
152
+ "canonical_renderer": "stream_copy" if audio_mode == "adopted-packet-copy" else "ffmpeg_mix",
153
+ }
70
154
  degraded = [
71
155
  {"source_path": clip["source_path"], "reason": clip["provenance_reason"]}
72
156
  for clip in video_clips
@@ -4,7 +4,6 @@ import json
4
4
  import re
5
5
  from pathlib import Path
6
6
 
7
- from artifacts import _artifact_fingerprint
8
7
  from assemble_constants import (
9
8
  SUBTITLE_STYLE_REF_H,
10
9
  VISUAL_OVERLAYS,
@@ -19,7 +18,7 @@ from source_subtitles import (
19
18
  _original_gap_subtitle_entries,
20
19
  _source_subtitle_mask_policy,
21
20
  )
22
- from subtitle_core import (
21
+ from subtitles.core import (
23
22
  _measured_subtitle_band,
24
23
  _measured_subtitle_safe_area,
25
24
  _normalize_subtitle_text,
@@ -136,7 +135,7 @@ def _load_visual_overlays(work_dir):
136
135
  """Return (overlays, source) from the canonical visual_overlays.json handoff."""
137
136
  path = Path(work_dir) / VISUAL_OVERLAYS
138
137
  if not path.exists():
139
- return [], {"present": False, "path": str(path), "fingerprint": None}
138
+ return [], {"present": False, "path": str(path)}
140
139
  try:
141
140
  data = json.loads(path.read_text(encoding="utf-8"))
142
141
  except json.JSONDecodeError as exc:
@@ -152,7 +151,6 @@ def _load_visual_overlays(work_dir):
152
151
  source = {
153
152
  "present": True,
154
153
  "path": str(path),
155
- "fingerprint": _artifact_fingerprint(path),
156
154
  "schema_version": 1,
157
155
  }
158
156
  return data["overlays"], source
@@ -236,14 +234,14 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
236
234
  if typ == "top_title":
237
235
  filt = (
238
236
  "drawtext="
239
- f"text='{safe_text}':x=(w-text_w)/2:y={int(bbox['y'])}:"
237
+ f"{_drawtext_font_option()}text='{safe_text}':x=(w-text_w)/2:y={int(bbox['y'])}:"
240
238
  f"fontsize={font_size}:fontcolor=white:borderw=2:bordercolor=black@0.85:"
241
239
  f"box=1:boxcolor=black@0.35:boxborderw=12:enable='{enable}'"
242
240
  )
243
241
  else:
244
242
  filt = (
245
243
  "drawtext="
246
- f"text='{safe_text}':x={int(bbox['x'])}:y={int(bbox['y'])}:"
244
+ f"{_drawtext_font_option()}text='{safe_text}':x={int(bbox['x'])}:y={int(bbox['y'])}:"
247
245
  f"fontsize={font_size}:fontcolor=white:borderw=2:bordercolor=black@0.85:"
248
246
  f"box=1:boxcolor=black@0.45:boxborderw=8:enable='{enable}'"
249
247
  )
@@ -352,8 +350,24 @@ def _escape_subtitle_filter_path(path):
352
350
 
353
351
 
354
352
  def _subtitle_burn_filter(subtitle_path):
355
- """Build the ffmpeg video filter used for hard-sub rendering."""
356
- return f"subtitles=filename='{_escape_subtitle_filter_path(subtitle_path)}'"
353
+ """Build the ffmpeg video filter used for hard-sub rendering.
354
+
355
+ With SUBTITLE_FONT_FILE set, libass also loads the fonts in that file's directory so
356
+ the ASS style's family name resolves to the declared file instead of a system font.
357
+ """
358
+ filt = f"subtitles=filename='{_escape_subtitle_filter_path(subtitle_path)}'"
359
+ font_file = CONFIG["subtitle_font_file"]
360
+ if font_file:
361
+ fonts_dir = Path(font_file).expanduser().resolve().parent
362
+ filt += f":fontsdir='{_escape_subtitle_filter_path(fonts_dir)}'"
363
+ return filt
364
+
365
+
366
+ def _drawtext_font_option():
367
+ font_file = CONFIG["subtitle_font_file"]
368
+ if not font_file:
369
+ return ""
370
+ return f"fontfile='{_escape_subtitle_filter_path(Path(font_file).expanduser().resolve())}':"
357
371
 
358
372
 
359
373
  def _output_downscale_filter(max_h):
@@ -3,8 +3,8 @@ name: video-cut
3
3
  user-invocable: false
4
4
  description: >
5
5
  把长视频按 Agent 选择的原片区间剪成短片。作为两阶段创作流程中的剪辑环节,读取 clip_plan.json 与源视频,
6
- 输出 edited_source.mp4;随后 Agent 按输出时间线写 narration.json。单独调用且未传 --no-narration-map 时,
7
- 仍支持旧版单阶段路径,把原片时间的 narration.json 映射为 narration_mapped.json。
6
+ 输出 edited_source.mp4;随后 Agent 按输出时间线写 narration.json。支持单视频与多视频(sources manifest)拼剪,
7
+ 本工具不读取、不映射旁白。
8
8
  触发词:视频剪辑、剪辑式解说、video cut、clip plan、拼剪。
9
9
  ---
10
10
 
@@ -15,10 +15,9 @@ description: >
15
15
  1. 校验并补全 `clip_plan.json`,写出带 `clip_id`、原片/输出时间与时长的 `clip_plan_validated.json`。
16
16
  2. 先避开原片硬切附近的闪帧风险,最后把边界吸附到可靠句末/自然停顿;声音完整性拥有最终优先级。
17
17
  3. 拼接选定区间,输出 `edited_source.mp4`。
18
- 4. 编排流程默认到此停止,由 Agent 按真实输出时间线写 `narration.json`。
19
- 5. 旧版单阶段路径还会把原片时间的旁白映射为 `narration_mapped.json`。
18
+ 4. 到此停止,由 Agent 按真实输出时间线写 `narration.json`;本工具不读取旁白,也不做原片→输出映射。
20
19
 
21
- 相同输入会得到相同输出。缓存仅表示:当 `edited_source.mp4` 新于 `clip_plan.json` 时复用成片。
20
+ 相同输入会得到相同输出。`edited_source.mp4.meta.json` 记录标准化 clips、渲染设置和每个源文件的 `size`/`mtime_ns`;三者与当前一致且 `edited_source.mp4` 存在非空才复用,任一不同即重渲染。只有 sidecar 而没有媒体文件不复用。
22
21
 
23
22
  ## 2. 输入契约
24
23
 
@@ -33,8 +32,6 @@ description: >
33
32
  - 多视频项目的每个片段还必须填写 `source_id`。
34
33
  - `speech_boundary_anchors.json` 与 ASR 时间段由理解阶段提供;Agent 先写大致区间,工具会尝试吸附并把仍在讲话区间内的入/出点作为 blocker 返回。
35
34
 
36
- `work_dir/narration.json` 只在旧版单阶段路径中可选读取;该路径要求旁白使用原片时间。若允许重复或重叠片段,旁白可带 `source_clip_id` 消歧。
37
-
38
35
  ## 3. 剪辑意图契约
39
36
 
40
37
  工具不会替 Agent 做创作选择。写片段前先完成本节的剪辑意图检查,并让每个区间映射到 `recap_story_plan.json` 的一个 beat。
@@ -47,7 +44,26 @@ beat_id | function | change | POV | preferred moment | 入点 reason | 出点 re
47
44
 
48
45
  不要因为“事件重要”就保留整段;要保留最能让 change 成立的具体表演、反应、动作或揭示。理解与情绪允许时晚进早出,同时保证台词、动作和技术边界完整。
49
46
 
50
- 下面的 `scripts/...` 均相对于本技能目录。若执行器从仓库根目录启动,请给脚本路径加上本技能的绝对目录。脚本不从其他技能目录读取文件;外部输入仅限命令显式传入的视频、参数与 `work_dir` 产物。
47
+ 对不能删去的问答、反应或动作兑现,先核源证据,再在同一 `clip_plan.json` 登记精确区间:
48
+
49
+ ```json
50
+ {
51
+ "clips": [{"start": 12, "end": 18}],
52
+ "required_evidence": {
53
+ "nodes": [
54
+ {"id": "refusal", "source": "/media/episode.mp4", "start": 12.25, "end": 14.5, "track": "audio", "content": "对方拒绝请求"},
55
+ {"id": "response", "source": "/media/episode.mp4", "start": 15, "end": 17.5, "track": "video", "content": "听到拒绝后的反应与决定"}
56
+ ],
57
+ "before": [["refusal", "response"]]
58
+ }
59
+ }
60
+ ```
61
+
62
+ `source` 使用实际源文件绝对路径,`start/end` 是原片秒;多源可另填 `source_id` 消歧。只登记确实需要保留的具体时刻,不将整个 beat 默认锁死。`before` 只登记本片必需的先后关系;无需约束顺序时写 `before: []`。
63
+
64
+ 工具在全部画面/句界吸附后检查每个必保时刻至少有一处完整连续保留、来源和先后;音频节点还检查源音轨是否存在。每次结果出现(包括局部片段)都需满足其声明的前提,不能用后面的完整段替开头缺前提的片段过关。结果写入 `clip_plan_validated.json.qc.required_evidence`;缺段、错序或无效声明会在预检、缓存复用和渲染前阻断,时长放宽选项不会跳过。该结果验证选段保留,实际语义与最终混音仍按审片步骤核对。
65
+
66
+ 下面的 `scripts/...` 均相对于本技能目录。若执行器从仓库根目录启动,请给脚本路径加上本技能的绝对目录。
51
67
 
52
68
  ## 4. 运行命令
53
69
 
@@ -60,20 +76,19 @@ python3 scripts/cut.py <video> --work-dir <work_dir> \
60
76
 
61
77
  - `clip_plan_validated.json`:标准化片段,包含 `clip_id`、`source_start/end`、`output_start/end` 与 `duration`。
62
78
  - `edited_source.mp4`:按计划拼接后的短视频。
63
- - `narration_mapped.json`:仅旧版单阶段路径生成;编排流程使用 `--no-narration-map`,不会生成该文件。
79
+ - `shot_review.json`:仅 `--review-shots` 开启后生成的实际视频短镜/密集切镜候选;不会更改计划。
64
80
 
65
- 编排流程下游把 `edited_source.mp4` 当作视频,把 Agent 按输出时间写的 `narration.json` 当作旁白。
81
+ 下游把 `edited_source.mp4` 当作视频,把 Agent 按输出时间写的 `narration.json` 当作旁白。
66
82
 
67
83
  ## 6. 边界与时间线规则
68
84
 
69
- - 旧版路径中,`clip_plan.json` 与 `narration.json` 都使用原片时间;本工具负责原片 → 输出映射。
70
- - 编排路径中,`narration.json` 直接使用剪后输出时间,不再映射。
71
- - 默认禁止重叠或重复原片区间;`--allow-overlap` 开启后,旁白应填写 `source_clip_id`。
85
+ - `clip_plan.json` 使用原片时间;`narration.json` 直接使用剪后输出时间,不存在原片 → 输出的旁白映射。
86
+ - 默认禁止重叠或重复原片区间;`--allow-overlap` 开启后才允许。
72
87
  - 片段起点只能位于源头、可靠句末/静音窗,或与上一片段构成无损同源连续连接;片段终点同理。ASR 判定仍在讲话且无法吸附时写入 `unsafe_clip_sentence_boundary` 并阻断。
73
88
  - `SCENE_CUT_SNAP` 默认开启:先按画面把 source start 向后、source end 向前吸附到附近硬切,随后句末吸附再做最终修正,避免视觉修正重新制造半句原声。默认范围为 `SCENE_CUT_SNAP_MARGIN=0.5` 秒,检测阈值为 `SCENE_CUT_DETECT_THRESHOLD=0.4`。
74
89
  - scene-change score 只提供接点候选,不证明接点自然。先检查短时间窗内是否出现密集候选,再区分来源:原片自带的无关短镜头整段删除;相关但短到像闪帧的镜头通过扩展 IN/OUT 保留完整动作、反应或台词,不用定格/慢放伪造时长;由本次拼接制造的切点则优先移动边界、恢复同源连续运动、合并相邻片段或改用更自然的连接,尽量消除。成片后仍要逐个播放接点前后约 0.5–1 秒;白闪或曝光叠化再结合逐帧亮度定位,不能为了通过视觉检测切断完整台词,也不能用转场遮掩坏接点。
90
+ - 修短残镜时不得仅为压低 scene 分数而对接点附近施加与所属镜头不连续的极端放大或位移;取景复核与修复验证流程见 `references/shot-review.md`。
75
91
  - 连续同源片段的无损连接不做句中双侧音频淡出;非连续片段仍在安全停顿内做防爆音淡入淡出。
76
- - 旧版旁白映射若跨越 clip 边界,不再裁短后继续:`clamped_beats` 永久阻断,`--allow-sparse-cut` 也不能绕过旁白句子完整性。
77
92
 
78
93
  需要检查短时间频繁切镜时,先用 ffmpeg scene filter 召回候选时间:
79
94
 
@@ -83,8 +98,11 @@ ffmpeg -i input.mp4 -vf "select='gt(scene,0.35)',showinfo" -an -f null -
83
98
 
84
99
  `0.35` 是起始阈值,不是质量判据;大幅运动、闪白和叠化都可能误报。把候选映射回原片 shot 与本次拼接边界后,按上面的来源分类处理,并以正常速度播放决定是否保留。
85
100
 
101
+ 需要精确到实际帧、检查长区间内部残镜并记录所用计划路径时,使用
102
+ `scripts/shot_review.py` 或 `cut.py --review-shots`(有黑边或包装时加 `--roi` / `--shot-roi`);
103
+ 详见 `references/shot-review.md`。
104
+
86
105
  ## 7. 能力边界
87
106
 
88
- - 不重新转写,也不做人物、剧情或情绪等语义理解;scene filter 只承担技术边界候选检测。
89
- - 不写旁白,也不替 Agent 选择片段;只消费 `clip_plan.json`。
107
+ - 不做语义理解,不写旁白,不替 Agent 选择片段;scene filter 只承担技术边界候选检测。
90
108
  - 只做生成 `edited_source.mp4` 所需的剪切、拼接与一次中间编码,不承担字幕包装或最终交付压缩。
@@ -0,0 +1,50 @@
1
+ # 成片内部短镜头与密集切镜召回
2
+
3
+ `shot_review.py` 是只读检查器,不改剪点、不触发吸附、不重编码视频,也不把短镜头自动删掉。
4
+ EDL 的一个长区间内仍可能藏着 2、12、14 帧反打;因此检查对象是**本轮实际渲染的视频**,不是只看计划里每段有多长。
5
+
6
+ ```bash
7
+ # 默认 --threshold 0.35:
8
+ python3 scripts/shot_review.py edited_source.mp4 --output review/shot_review.json
9
+ # 只有当前 video-cut 的输出与 sidecar 齐全,才额外关联当前剪辑计划:
10
+ python3 scripts/shot_review.py edited_source.mp4 --plan clip_plan_validated.json \
11
+ --output review/shot_review_planned.json
12
+ # 怀疑漏检时另跑一轮更低阈值做对照;0.08 只是一个更高召回的取值示例,不是规定的第二遍:
13
+ python3 scripts/shot_review.py edited_source.mp4 \
14
+ --threshold 0.08 --output review/shot_review_sensitive.json
15
+ # 有黑边/包装时,按实测主画窗填入像素坐标,保持阈值不变做对照:
16
+ python3 scripts/shot_review.py packaged.mp4 --roi "$X" "$Y" "$WIDTH" "$HEIGHT" \
17
+ --output review/shot_review_picture.json
18
+ # 正常 cut 后显式开启;缓存命中也查实际文件;normalize-only 不扫描:
19
+ python3 scripts/cut.py source.mp4 --work-dir work --review-shots
20
+ # 同一画窗/阈值参数也可传给 cut.py --review-shots --shot-roi X Y WIDTH HEIGHT --shot-scene-threshold T
21
+ ```
22
+
23
+ `--roi` 仅裁检测输入,不改视频文件或剪辑计划。坐标采用 FFmpeg 自动转正后的原生像素,
24
+ 不是播放器按 SAR 拉伸后的显示尺寸;矩形需完整位于画布内。报告的 `scene_roi` 保存实际
25
+ 检测范围,未指定时为 `null`(全画布)。先排除黑边及标题/花字区,再调召回阈值;
26
+ 同时保留全画布或更大范围的对照,不能通过缩小范围隐藏问题。画窗随镜头变化时,
27
+ 一个固定 ROI 只能覆盖该区域,不能据此宣称已检查所有画面。
28
+
29
+ ## 精度和边界
30
+
31
+ - `ffprobe` 完整解码收集真实帧 PTS;scene filter 使用整数 PTS 与过滤器 timebase,不用 seek 后的浮点 `pts_time` 反推帧号。
32
+ - 帧号从 0 开始,区间半开 `[start_frame,end_frame)`;首镜、末镜也检查。VFR 同时记录真实 rational PTS 时长,不能用平均 fps 乘秒数。
33
+ - 默认候选规则:镜头不超过 **1 秒**,或 **2 秒内至少 4 个切点**。帧数上限缺省由实测帧钟换算(`round(fps × max_short_seconds)`),不写死某个帧率;需要固定帧数时用 `--max-short-frames` 显式覆盖。参数只控制召回,不是统一剪辑标准。
34
+ - `0.35` 是默认 scene 起始阈值。加黑边、包装占比大、低反差的画面会系统性少报。**某个阈值下零候选不是没有闪帧的证据。** 用一段已知有坏短镜的窗口校准阈值,保留每一轮的报告,不静默换阈值只报告“通过”。降低阈值也可能增加曝光、运动和动效误报。
35
+ - 未提供计划时来源为 `UNKNOWN`。提供计划时先核对计划 clips、渲染设置与 `edited_source.mp4.meta.json` 一致、源文件 `size`/`mtime_ns` 未变、输出存在非空;失配直接失败,不猜“最新版本”。报告 `plan_binding.path` 只记录所用计划路径。
36
+ - 靠近量化后 EDL 拼接点一帧以内仅标 `EDIT_JOIN_CANDIDATE`。内部候选可以标所在已绑定 clip、估计原片秒数,但仍为 `UNKNOWN`;没有独立源片证据,不能认定是原片自带切镜。
37
+ - 未知时码、末帧时长无法覆盖到流末尾、解码失败均阻断扫描。报告先置 `SCANNING`,失败写 `SCAN_FAILED`,不留下旧成功报告冒充本轮结果。仅新路径或可识别为本工具 schema 的旧报告可写;报告路径不能覆盖计划与元数据双方声明的视频/源文件,即使二者已经过期失配。
38
+
39
+ ## 如何处理候选
40
+
41
+ 对每个候选看前后 0.5–1 秒,核对角色表演、完整动作、对白与原片切镜;未实际播放就保留 `NEEDS_DYNAMIC_REVIEW`。
42
+ 先对照源画面和取景参数:同一原生镜头中途换 crop/画窗,或曝光闪光,都可能产生切点分数;两个候选间的帧数不一定是一段真实短镜头。
43
+ 修复后用同样参数重跑对照:候选消失才算真的修掉;仍被保留的短镜逐个判断,不因“短”就判错。
44
+ 判断无关残镜后,在作者计划里删除整个无关镜头或恢复同源连续画面;相关反应可回原片扩完整,不能用定格补长、转场掩盖或只修导出文件。
45
+
46
+ ## 修短残镜时的取景复核
47
+
48
+ 修短残镜时,同时核对原片镜头变化与输出 `crop/window` 变化。不得仅为压低 scene 分数,对接点前后少量帧施加与所属镜头不连续的极端放大或位移,造成关键表情、动作被裁或清晰度明显下降。先在原片自然切点上分别确定相邻镜头各自连续、清晰、主体完整的取景;不同镜头不要求景别或裁幅一致,但尺度变化、主体位置及视线/运动方向须在正常速度下复核。保存修复前后相同输出帧号的邻帧对照,再核声音、字幕、总帧数与播放速度。候选数量或 scene 分数下降,只说明检测结果改变,不证明接点已经修复;没有正常速度观看能力时保留未检查状态。
49
+
50
+ `NO_CANDIDATES` 仅表示该阈值下没有短镜/密集切镜候选;不代表故事、听感、动态观感或发布授权通过。报告始终保留 `normal_speed_review=NOT_CHECKED`,不会写入已有 `clip_plan_validated.json['qc']`。
@@ -3,30 +3,15 @@
3
3
 
4
4
  from cut_cli import main
5
5
  from cut_contract import (
6
- cut_plan_fingerprint,
7
6
  edited_source_render_cache_payload,
8
- edited_source_render_fingerprint,
9
- file_fingerprint,
10
- load_clip_plan,
11
7
  normalize_clip_plan,
12
8
  normalize_multi_source_clip_plan,
13
- normalize_sources_manifest,
14
9
  parse_duration_seconds,
15
10
  should_reuse_edited_source,
16
- value_fingerprint,
17
- )
18
- from cut_render import (
19
- build_edited_source_video,
20
- update_delivery_qc,
21
- write_cut_delivery_qc,
22
11
  )
12
+ from cut_render import build_edited_source_video
23
13
  from media_geometry import VideoGeometry
24
- from narration_mapping import (
25
- lint_mapped_narration,
26
- map_narration_to_clips,
27
- source_time_to_output_time,
28
- update_cut_qc,
29
- )
14
+ from narration_mapping import update_cut_qc
30
15
  from sentence_boundaries import (
31
16
  enforce_clip_sentence_boundaries,
32
17
  snap_clip_ends_to_lines,
@@ -38,29 +23,18 @@ from sentence_boundaries import (
38
23
  __all__ = [
39
24
  "VideoGeometry",
40
25
  "build_edited_source_video",
41
- "cut_plan_fingerprint",
42
26
  "edited_source_render_cache_payload",
43
- "edited_source_render_fingerprint",
44
27
  "enforce_clip_sentence_boundaries",
45
- "file_fingerprint",
46
- "lint_mapped_narration",
47
- "load_clip_plan",
48
28
  "main",
49
- "map_narration_to_clips",
50
29
  "normalize_clip_plan",
51
30
  "normalize_multi_source_clip_plan",
52
- "normalize_sources_manifest",
53
31
  "parse_duration_seconds",
54
32
  "should_reuse_edited_source",
55
33
  "snap_clip_ends_to_lines",
56
34
  "snap_clip_starts_to_lines",
57
35
  "snap_clips_off_shot_changes",
58
36
  "snap_multi_source_clips",
59
- "source_time_to_output_time",
60
37
  "update_cut_qc",
61
- "update_delivery_qc",
62
- "value_fingerprint",
63
- "write_cut_delivery_qc",
64
38
  ]
65
39
 
66
40
  if __name__ == "__main__":
@@ -1,11 +1,13 @@
1
1
  """Command-line orchestration for the video-cut skill."""
2
2
 
3
3
  import json
4
+ import math
4
5
 
5
6
 
6
7
  from pathlib import Path
7
8
 
8
9
  from lib import CONFIG, get_video_duration, log
10
+ import shot_review
9
11
 
10
12
  from cut_contract import (
11
13
  _write_edited_source_meta,
@@ -14,19 +16,15 @@ from cut_contract import (
14
16
  normalize_multi_source_clip_plan,
15
17
  parse_duration_seconds,
16
18
  should_reuse_edited_source,
17
- value_fingerprint,
18
19
  )
19
20
  from cut_render import (
20
21
  build_edited_source_video,
21
22
  update_delivery_qc,
22
23
  write_cut_delivery_qc,
23
24
  )
24
- from media_geometry import _select_output_geometry
25
- from narration_mapping import (
26
- lint_mapped_narration,
27
- map_narration_to_clips,
28
- update_cut_qc,
29
- )
25
+ from media_geometry import _has_audio_stream, _select_output_geometry
26
+ from narrative_selection import check_required_evidence
27
+ from narration_mapping import update_cut_qc
30
28
  from sentence_boundaries import (
31
29
  _combine_boundary_windows,
32
30
  _load_sentence_boundary_windows,
@@ -44,13 +42,13 @@ def main():
44
42
  import argparse
45
43
 
46
44
  parser = argparse.ArgumentParser(
47
- description="video-cut: build an edited source video from an agent clip plan and map narration onto the cut timeline."
45
+ description="video-cut: build edited_source.mp4 from an agent clip plan; narration is authored afterwards on the output timeline."
48
46
  )
49
47
  parser.add_argument("video", help="source video path")
50
48
  parser.add_argument(
51
49
  "--work-dir",
52
50
  required=True,
53
- help="dir holding clip_plan.json (and optionally narration.json)",
51
+ help="dir holding clip_plan.json",
54
52
  )
55
53
  parser.add_argument(
56
54
  "--clip-plan",
@@ -62,11 +60,6 @@ def main():
62
60
  default=None,
63
61
  help="multi-source manifest json mapping source_id values to source media",
64
62
  )
65
- parser.add_argument(
66
- "--narration",
67
- default=None,
68
- help="narration json to map (default: <work-dir>/narration.json)",
69
- )
70
63
  parser.add_argument(
71
64
  "--target-duration",
72
65
  default=None,
@@ -86,19 +79,20 @@ def main():
86
79
  parser.add_argument(
87
80
  "--normalize-only",
88
81
  action="store_true",
89
- help="only normalize the clip plan -> clip_plan_validated.json (no render/map); "
90
- "lets validate lint the SAME padded/pruned plan the mapper uses",
82
+ help="only normalize the clip plan -> clip_plan_validated.json (no render); "
83
+ "lets validate lint the SAME padded/pruned plan the render uses",
91
84
  )
92
85
  parser.add_argument(
93
- "--no-narration-map",
94
- action="store_true",
95
- help="render edited_source.mp4 but do NOT map narration.json onto the cut "
96
- "(cut-first/narrate-second: narration is authored in OUTPUT time, no mapping)",
86
+ "--review-shots", action="store_true",
87
+ help="scan actual rendered/reused video for internal short-shot and dense-cut candidates; never repair",
97
88
  )
98
89
  parser.add_argument(
99
- "--allow-sparse-cut",
100
- action="store_true",
101
- help="do not block on heavy narration drop / sparse output (e.g. an intentional montage)",
90
+ "--shot-scene-threshold", type=float, default=None,
91
+ help="explicit scene recall threshold for --review-shots (default 0.35; not an acceptance criterion)",
92
+ )
93
+ parser.add_argument(
94
+ "--shot-roi", nargs=4, type=int, metavar=("X", "Y", "WIDTH", "HEIGHT"),
95
+ help="scan only this pixel rectangle with --review-shots; never crop the rendered video",
102
96
  )
103
97
  parser.add_argument(
104
98
  "--allow-duration-drift",
@@ -106,6 +100,15 @@ def main():
106
100
  help="do not block when validated clip duration is far from --target-duration",
107
101
  )
108
102
  args = parser.parse_args()
103
+ if args.shot_scene_threshold is not None and (
104
+ not args.review_shots or not math.isfinite(args.shot_scene_threshold)
105
+ or not 0 <= args.shot_scene_threshold <= 1
106
+ ):
107
+ parser.error("--shot-scene-threshold requires --review-shots and a finite value in [0,1]")
108
+ if args.shot_roi is not None and (
109
+ not args.review_shots or min(args.shot_roi[:2]) < 0 or min(args.shot_roi[2:]) <= 0
110
+ ):
111
+ parser.error("--shot-roi requires --review-shots, nonnegative X/Y and positive WIDTH/HEIGHT")
109
112
 
110
113
  # CLIP_PADDING is declared in every skill's CONFIG, but video-cut is the only place that
111
114
  # implements padding — and it used to read the CLI flag alone, so setting the env var did
@@ -201,7 +204,6 @@ def main():
201
204
  start_max_trim=CONFIG["clip_start_snap_max_trim"],
202
205
  )
203
206
 
204
- validated_plan["raw_plan_fingerprint"] = value_fingerprint(raw_plan)
205
207
  validated_plan.setdefault("qc", {})["join_fade_ms"] = round(
206
208
  CONFIG["clip_join_audio_fade_ms"], 3
207
209
  )
@@ -214,17 +216,30 @@ def main():
214
216
  _, _, _, geometry_qc = _select_output_geometry(source_paths, validated_plan["clips"])
215
217
  validated_plan["qc"]["output_geometry"] = geometry_qc
216
218
  validated_plan["qc"]["output_geometry_reason"] = geometry_qc["reason"]
217
- allow_duration_drift = bool(args.allow_duration_drift or args.allow_sparse_cut)
218
- drift_source = (
219
- "--allow-duration-drift"
220
- if args.allow_duration_drift
221
- else ("--allow-sparse-cut" if args.allow_sparse_cut else None)
222
- )
223
219
  update_cut_qc(
224
220
  validated_plan,
225
- allow_duration_drift=allow_duration_drift,
226
- duration_drift_allowed_by=drift_source,
221
+ allow_duration_drift=bool(args.allow_duration_drift),
222
+ duration_drift_allowed_by="--allow-duration-drift" if args.allow_duration_drift else None,
227
223
  )
224
+ if isinstance(raw_plan, dict) and 'required_evidence' in raw_plan:
225
+ # Re-evaluate the final snapped ranges even when the media cache can be reused.
226
+ # A prior rendered receipt must not survive a failed revision preflight.
227
+ (work_dir / 'cut_delivery_qc.json').unlink(missing_ok=True)
228
+ contract = raw_plan['required_evidence']
229
+ plan_sources = {str(Path(path).resolve()): path for path in source_paths}
230
+ source_audio = {}
231
+
232
+ def has_source_audio(source):
233
+ # Probed lazily, once per source, only for validated audio nodes.
234
+ if source not in source_audio:
235
+ source_audio[source] = source in plan_sources and _has_audio_stream(plan_sources[source])
236
+ return source_audio[source]
237
+
238
+ report = check_required_evidence(contract, validated_plan, input_video=args.video,
239
+ source_audio=has_source_audio)
240
+ validated_plan['qc']['required_evidence'] = {**report, 'contract': contract}
241
+ if report['selection_status'] == 'BLOCK':
242
+ validated_plan['qc'].setdefault('blocking', []).extend(report['findings'])
228
243
  update_delivery_qc(
229
244
  validated_plan,
230
245
  source_paths=source_paths,
@@ -235,7 +250,7 @@ def main():
235
250
  )
236
251
  if validated_plan["qc"].get("blocking"):
237
252
  raise SystemExit(
238
- "clip_plan QC blocking: fix unsafe sentence boundaries or target-duration drift. "
253
+ "clip_plan QC blocking: fix required source evidence, unsafe sentence boundaries or target-duration drift. "
239
254
  "Only duration drift can be explicitly accepted with --allow-duration-drift; "
240
255
  "sentence truncation is never allowed. See clip_plan_validated.json['qc']."
241
256
  )
@@ -277,36 +292,17 @@ def main():
277
292
  json.dumps(validated_plan, ensure_ascii=False, indent=2), encoding="utf-8"
278
293
  )
279
294
 
280
- narration_path = (
281
- Path(args.narration) if args.narration else work_dir / "narration.json"
282
- )
283
- if narration_path.exists() and not args.no_narration_map:
284
- narration = json.loads(narration_path.read_text(encoding="utf-8"))
285
- mapped = map_narration_to_clips(narration, validated_plan)
286
- if not mapped:
287
- raise SystemExit("narration 没有落入 clip_plan 片段内的有效解说")
288
- (work_dir / "narration_mapped.json").write_text(
289
- json.dumps(mapped, ensure_ascii=False, indent=2), encoding="utf-8"
290
- )
291
- log(f"映射解说 {len(mapped)} 段 → narration_mapped.json")
292
- report = lint_mapped_narration(
293
- mapped, len(narration), validated_plan["total_duration"]
295
+ if args.review_shots:
296
+ review_options = {"plan_path": work_dir / "clip_plan_validated.json"}
297
+ if args.shot_scene_threshold is not None:
298
+ review_options["threshold"] = args.shot_scene_threshold
299
+ if args.shot_roi is not None:
300
+ review_options["roi"] = args.shot_roi
301
+ shot_review.write_scan(
302
+ edited_source_path, work_dir / "shot_review.json",
303
+ **review_options,
294
304
  )
295
- (work_dir / "narration_mapped_lint.json").write_text(
296
- json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
297
- )
298
- for w in report["warnings"]:
299
- log(f" ⚠️ 剪后解说同步: {w['message']} [{w['code']}]")
300
- if report["clamped_count"]:
301
- raise SystemExit(
302
- "剪后解说有句子被 clip 边界裁断。移动 clip_plan 边界或重写整句后重跑;"
303
- "--allow-sparse-cut 不能跳过语句完整性门禁。详见 narration_mapped_lint.json"
304
- )
305
- if report["blocking"] and not args.allow_sparse_cut:
306
- raise SystemExit(
307
- "剪后解说与保留片段对不上:丢弃过多或成片过稀疏。改 narration.json / clip_plan.json "
308
- "让解说落在保留片段内后重跑,或加 --allow-sparse-cut 接受当前映射。详见 narration_mapped_lint.json"
309
- )
305
+
310
306
  log(
311
307
  f"剪辑模式: {len(validated_plan['clips'])} 个片段 → {validated_plan['total_duration']:.1f}s"
312
308
  )