@oh-story/dsh 0.1.10 → 0.1.12

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 +11 -11
  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 +41 -41
  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
@@ -0,0 +1,244 @@
1
+ """Validate explicitly required source spans in a normalized constant-speed cut plan.
2
+
3
+ This checks source-span selection and ordering only. It does not claim that the
4
+ rendered mix is audible or that the declared content is semantically correct.
5
+ """
6
+
7
+ import math
8
+ import os
9
+ from typing import TypeGuard
10
+
11
+
12
+ _EPSILON = 1e-9
13
+
14
+
15
+ def _invalid(message):
16
+ return {
17
+ "selection_status": "BLOCK",
18
+ "nodes": [],
19
+ "findings": [
20
+ {
21
+ "code": "REQUIRED_EVIDENCE_INVALID",
22
+ "message": message,
23
+ }
24
+ ],
25
+ "semantic_status": "NOT_CHECKED",
26
+ }
27
+
28
+
29
+ def _realpath(value, *, require_absolute=True):
30
+ if not isinstance(value, str) or (require_absolute and not os.path.isabs(value)):
31
+ raise ValueError("required evidence source must be an absolute path")
32
+ return os.path.realpath(value)
33
+
34
+
35
+ def _finite_nonnegative(value: object) -> TypeGuard[int | float]:
36
+ return (
37
+ isinstance(value, (int, float))
38
+ and not isinstance(value, bool)
39
+ and math.isfinite(value)
40
+ and value >= 0
41
+ )
42
+
43
+
44
+ def _validate_contract(contract):
45
+ if not isinstance(contract, dict):
46
+ raise ValueError("required_evidence must be an object")
47
+ nodes = contract.get("nodes")
48
+ if not isinstance(nodes, list) or not nodes:
49
+ raise ValueError("required_evidence.nodes must be a non-empty array")
50
+ if not isinstance(contract.get("before"), list):
51
+ raise ValueError("required_evidence.before must be an array")
52
+
53
+ normalized = []
54
+ ids = set()
55
+ for index, node in enumerate(nodes, 1):
56
+ if not isinstance(node, dict):
57
+ raise ValueError(f"required evidence node #{index} must be an object")
58
+ node_id = node.get("id")
59
+ if not isinstance(node_id, str) or not node_id.strip() or node_id in ids:
60
+ raise ValueError("required evidence node ids must be unique non-empty strings")
61
+ ids.add(node_id)
62
+ track = node.get("track")
63
+ if track not in {"audio", "video"}:
64
+ raise ValueError(f"required evidence node {node_id} has invalid track")
65
+ content = node.get("content")
66
+ if not isinstance(content, str) or not content.strip():
67
+ raise ValueError(f"required evidence node {node_id} has empty content")
68
+ start, end = node.get("start"), node.get("end")
69
+ if not (_finite_nonnegative(start) and _finite_nonnegative(end) and end > start):
70
+ raise ValueError(f"required evidence node {node_id} has invalid start/end")
71
+ source_id = node.get("source_id")
72
+ if "source_id" in node and (
73
+ not isinstance(source_id, str) or not source_id.strip()
74
+ ):
75
+ raise ValueError(f"required evidence node {node_id} has invalid source_id")
76
+
77
+ item = {
78
+ "id": node_id,
79
+ "track": track,
80
+ "content": content,
81
+ "source": _realpath(node.get("source")),
82
+ "start": start,
83
+ "end": end,
84
+ }
85
+ if "source_id" in node:
86
+ item["source_id"] = source_id
87
+ normalized.append(item)
88
+
89
+ edges = []
90
+ for edge in contract["before"]:
91
+ if (
92
+ not isinstance(edge, list)
93
+ or len(edge) != 2
94
+ or edge[0] not in ids
95
+ or edge[1] not in ids
96
+ or edge[0] == edge[1]
97
+ ):
98
+ raise ValueError("required_evidence.before contains an invalid node pair")
99
+ edges.append((edge[0], edge[1]))
100
+ return normalized, edges
101
+
102
+
103
+ def _same(value, expected):
104
+ return abs(value - expected) <= _EPSILON
105
+
106
+
107
+ def _matching_segments(node, validated_plan, input_video):
108
+ segments = []
109
+ for clip in validated_plan["clips"]:
110
+ source = _realpath(
111
+ os.fspath(clip.get("source_path", input_video)), require_absolute=False
112
+ )
113
+ if source != node["source"]:
114
+ continue
115
+ if "source_id" in node and clip.get("source_id") != node["source_id"]:
116
+ continue
117
+
118
+ clip_start = clip["source_start"]
119
+ clip_end = clip["source_end"]
120
+ start = max(node["start"], clip_start)
121
+ end = min(node["end"], clip_end)
122
+ if end <= start:
123
+ continue
124
+ output_start = clip["output_start"] + start - clip_start
125
+ segments.append(
126
+ {
127
+ "source_id": clip.get("source_id"),
128
+ "source_start": start,
129
+ "source_end": end,
130
+ "output_start": output_start,
131
+ "output_end": output_start + end - start,
132
+ }
133
+ )
134
+ return sorted(segments, key=lambda item: item["output_start"])
135
+
136
+
137
+ def _occurrences(node, validated_plan, input_video):
138
+ segments = _matching_segments(node, validated_plan, input_video)
139
+ occurrences = []
140
+ for index, segment in enumerate(segments):
141
+ if not _same(segment["source_start"], node["start"]):
142
+ continue
143
+ source_cursor = segment["source_end"]
144
+ output_cursor = segment["output_end"]
145
+ for following in segments[index + 1 :]:
146
+ if source_cursor >= node["end"] - _EPSILON:
147
+ break
148
+ if (
149
+ following["source_id"] == segment["source_id"]
150
+ and _same(following["source_start"], source_cursor)
151
+ and _same(following["output_start"], output_cursor)
152
+ ):
153
+ source_cursor = following["source_end"]
154
+ output_cursor = following["output_end"]
155
+ if _same(source_cursor, node["end"]):
156
+ occurrences.append(
157
+ {"start": segment["output_start"], "end": output_cursor}
158
+ )
159
+ return occurrences
160
+
161
+
162
+ def check_required_evidence(contract, validated_plan, *, input_video, source_audio) -> dict:
163
+ """Return bounded source-selection QC for a declared required-evidence contract.
164
+
165
+ `source_audio(realpath) -> bool` is consulted only for audio nodes, after the contract
166
+ has been validated, so callers never pre-walk the raw contract to decide whether to probe."""
167
+ try:
168
+ nodes, edges = _validate_contract(contract)
169
+ except (AttributeError, KeyError, TypeError, ValueError) as exc:
170
+ return _invalid(str(exc))
171
+
172
+ findings = []
173
+ occurrences_by_id = {}
174
+ fragment_starts_by_id = {}
175
+ report_nodes = []
176
+ for node in nodes:
177
+ if node["track"] == "audio" and source_audio(node["source"]) is not True:
178
+ occurrences = []
179
+ fragment_starts = []
180
+ findings.append(
181
+ {
182
+ "code": "REQUIRED_EVIDENCE_AUDIO_UNAVAILABLE",
183
+ "message": (
184
+ f"audio node {node['id']} requires a source with an audio stream"
185
+ ),
186
+ "node_id": node["id"],
187
+ }
188
+ )
189
+ else:
190
+ fragment_starts = [
191
+ segment["output_start"]
192
+ for segment in _matching_segments(node, validated_plan, input_video)
193
+ ]
194
+ occurrences = _occurrences(node, validated_plan, input_video)
195
+ if not occurrences:
196
+ findings.append(
197
+ {
198
+ "code": "REQUIRED_EVIDENCE_MISSING",
199
+ "message": (
200
+ f"node {node['id']} is not retained as one continuous source "
201
+ "and output span"
202
+ ),
203
+ "node_id": node["id"],
204
+ }
205
+ )
206
+ occurrences_by_id[node["id"]] = occurrences
207
+ fragment_starts_by_id[node["id"]] = fragment_starts
208
+ report_nodes.append({**node, "occurrences": occurrences})
209
+
210
+ for premise_id, result_id in edges:
211
+ premise_occurrences = occurrences_by_id[premise_id]
212
+ result_fragment_starts = fragment_starts_by_id[result_id]
213
+ unpreceded_starts = [
214
+ result_start
215
+ for result_start in result_fragment_starts
216
+ if not any(
217
+ premise["end"] <= result_start + _EPSILON
218
+ for premise in premise_occurrences
219
+ )
220
+ ]
221
+ ordered = bool(result_fragment_starts) and not unpreceded_starts
222
+ if not ordered:
223
+ detail = (
224
+ f"; earliest unpreceded output start={min(unpreceded_starts):.9g}s"
225
+ if unpreceded_starts
226
+ else "; no matching result fragment was retained"
227
+ )
228
+ findings.append(
229
+ {
230
+ "code": "REQUIRED_EVIDENCE_ORDER",
231
+ "message": (
232
+ f"every retained fragment of {result_id} must follow a complete "
233
+ f"occurrence of {premise_id}{detail}"
234
+ ),
235
+ "node_id": result_id,
236
+ }
237
+ )
238
+
239
+ return {
240
+ "selection_status": "BLOCK" if findings else "PASS",
241
+ "nodes": report_nodes,
242
+ "findings": findings,
243
+ "semantic_status": "NOT_CHECKED",
244
+ }
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only internal-shot recall on the actual rendered video; never repair an EDL."""
3
+ import argparse
4
+ from bisect import bisect_left
5
+ from fractions import Fraction
6
+ import json
7
+ import math
8
+ import os
9
+ from pathlib import Path
10
+ import re
11
+ import subprocess
12
+ import tempfile
13
+
14
+ from cut_contract import edited_source_render_cache_payload
15
+ from lib import file_identity
16
+
17
+
18
+ def _positive(value, name, *, integer=False):
19
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
20
+ raise ValueError(f"{name} must be a positive number")
21
+ if not math.isfinite(value) or value <= 0 or (integer and not isinstance(value, int)):
22
+ raise ValueError(f"invalid {name}")
23
+
24
+
25
+ def _validate_clock(pts, end):
26
+ if not pts or any(not isinstance(p, Fraction) for p in [*pts, end]):
27
+ raise ValueError("frame clock requires exact rational PTS")
28
+ if pts[0] != 0 or any(a >= b for a, b in zip(pts, pts[1:])) or end <= pts[-1]:
29
+ raise ValueError("invalid/nonmonotonic frame clock")
30
+
31
+
32
+ def summarize_candidates(pts, end, cut_frames, *, max_short_frames=None,
33
+ max_short_seconds=1.0, dense_window_seconds=2.0,
34
+ min_dense_cuts=4):
35
+ """Half-open frame spans including the head and tail; thresholds are recall policy."""
36
+ _validate_clock(pts, end)
37
+ _positive(max_short_seconds, "max_short_seconds")
38
+ if max_short_frames is None:
39
+ # A fixed frame count only coincides with the seconds limit at one frame rate, so
40
+ # derive it from the measured clock; --max-short-frames stays an explicit override.
41
+ measured_fps = Fraction(len(pts), 1) / end
42
+ max_short_frames = max(1, round(measured_fps * Fraction(str(max_short_seconds))))
43
+ _positive(max_short_frames, "max_short_frames", integer=True)
44
+ _positive(dense_window_seconds, "dense_window_seconds")
45
+ _positive(min_dense_cuts, "min_dense_cuts", integer=True)
46
+ if any(type(f) is not int or not 0 < f < len(pts) for f in cut_frames):
47
+ raise ValueError("invalid candidate frame index")
48
+ cuts = sorted(set(cut_frames))
49
+ boundaries = [0, *cuts, len(pts)]
50
+ clock = [*pts, end]
51
+ short = []
52
+ for start, stop in zip(boundaries, boundaries[1:]):
53
+ duration = clock[stop] - clock[start]
54
+ if stop - start <= max_short_frames and duration <= Fraction(str(max_short_seconds)):
55
+ short.append({
56
+ "start_frame": start, "end_frame": stop, "frame_count": stop - start,
57
+ "start_exact": str(clock[start]), "end_exact": str(clock[stop]),
58
+ "duration_exact": str(duration), "duration_seconds": float(duration),
59
+ "review_status": "NEEDS_DYNAMIC_REVIEW",
60
+ })
61
+ windows = []
62
+ right = 0
63
+ window_duration = Fraction(str(dense_window_seconds))
64
+ for left in range(len(cuts)):
65
+ right = max(right, left)
66
+ while right < len(cuts) and pts[cuts[right]] - pts[cuts[left]] <= window_duration:
67
+ right += 1
68
+ if right - left < min_dense_cuts:
69
+ continue
70
+ members = cuts[left:right]
71
+ if windows and members[0] <= windows[-1]["cut_frames"][-1]:
72
+ windows[-1]["cut_frames"] = sorted(set(windows[-1]["cut_frames"]) | set(members))
73
+ else:
74
+ windows.append({"cut_frames": members, "review_status": "NEEDS_DYNAMIC_REVIEW"})
75
+ for window in windows:
76
+ window.update({"start_exact": str(pts[window["cut_frames"][0]]),
77
+ "end_exact": str(pts[window["cut_frames"][-1]])})
78
+ return {
79
+ "status": "NEEDS_REVIEW" if short or windows else "NO_CANDIDATES",
80
+ "normal_speed_review": "NOT_CHECKED", "automatic_repairs": [],
81
+ "candidates": [{"frame": f, "pts_exact": str(pts[f]), "origin": "UNKNOWN"} for f in cuts],
82
+ "short_spans": short, "dense_windows": windows,
83
+ "policy": {"max_short_frames": max_short_frames, "max_short_seconds": max_short_seconds,
84
+ "dense_window_seconds": dense_window_seconds, "min_dense_cuts": min_dense_cuts},
85
+ }
86
+
87
+
88
+ def probe_frame_clock(video):
89
+ result = subprocess.run([
90
+ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_frames",
91
+ "-show_entries", "stream=time_base,start_pts,duration_ts:frame=pts,duration,pkt_duration",
92
+ "-of", "json", str(video),
93
+ ], capture_output=True, text=True, timeout=600)
94
+ if result.returncode or result.stderr.strip():
95
+ raise RuntimeError(f"frame decode probe failed: {result.stderr[-2000:]}")
96
+ try:
97
+ data = json.loads(result.stdout)
98
+ stream = data["streams"][0]
99
+ tb = Fraction(stream["time_base"])
100
+ if tb <= 0:
101
+ raise ValueError("nonpositive time base")
102
+ frames = data["frames"]
103
+ absolute = [int(f["pts"]) * tb for f in frames]
104
+ origin = absolute[0]
105
+ pts = [p - origin for p in absolute]
106
+ last_duration = frames[-1].get("duration", frames[-1].get("pkt_duration"))
107
+ if last_duration is None or int(last_duration) <= 0:
108
+ raise ValueError("missing/invalid terminal frame duration")
109
+ decoded_end = pts[-1] + int(last_duration) * tb
110
+ if "duration_ts" in stream:
111
+ end = (int(stream.get("start_pts", frames[0]["pts"])) + int(stream["duration_ts"])) * tb - origin
112
+ if abs(end - decoded_end) > tb:
113
+ raise ValueError("decoded frame coverage does not reach declared stream end")
114
+ else:
115
+ end = decoded_end
116
+ _validate_clock(pts, end)
117
+ except (KeyError, IndexError, ValueError, TypeError, ZeroDivisionError) as exc:
118
+ raise ValueError(f"cannot establish exact frame clock: {exc}") from exc
119
+ return pts, end, origin
120
+
121
+
122
+ def _validate_scene_roi(roi, video):
123
+ """Validate an ROI (four ints from the CLI) against the video's auto-oriented native frame."""
124
+ if len(roi) != 4:
125
+ raise ValueError("scene ROI must contain exactly four integers")
126
+ x, y, width, height = roi
127
+ if x < 0 or y < 0 or width <= 0 or height <= 0:
128
+ raise ValueError("scene ROI requires x/y >= 0 and width/height > 0")
129
+
130
+ from media_geometry import _probe_video_geometry
131
+ facts = _probe_video_geometry(video).facts
132
+ rotation = facts["rotation"]
133
+ if rotation not in {0, 90, 180, 270}:
134
+ raise ValueError("scene ROI requires a right-angle video rotation")
135
+ canvas_width, canvas_height = (
136
+ (facts["coded_height"], facts["coded_width"]) if rotation in {90, 270}
137
+ else (facts["coded_width"], facts["coded_height"])
138
+ )
139
+ if x + width > canvas_width or y + height > canvas_height:
140
+ raise ValueError(
141
+ f"scene ROI exceeds auto-oriented native frame {canvas_width}x{canvas_height}"
142
+ )
143
+
144
+
145
+ def detect_scene_pts(video, threshold, roi=None):
146
+ """No seek or float pts_time: showinfo integer pts + its actual filter timebase.
147
+
148
+ The single library-side check of threshold/ROI; the CLIs pre-check with parser.error."""
149
+ if isinstance(threshold, bool) or not isinstance(threshold, (int, float)) or not math.isfinite(threshold) or not 0 <= threshold <= 1:
150
+ raise ValueError("scene threshold must be finite and in [0,1]")
151
+ filters = []
152
+ if roi is not None:
153
+ _validate_scene_roi(roi, video)
154
+ x, y, width, height = roi
155
+ filters.append(f"crop={width}:{height}:{x}:{y}:exact=1")
156
+ filters.extend([f"select='gt(scene,{threshold})'", "showinfo"])
157
+ result = subprocess.run([
158
+ "ffmpeg", "-hide_banner", "-nostdin", "-v", "info", "-xerror", "-copyts",
159
+ "-threads", "2", "-i", str(video), "-map", "0:v:0",
160
+ "-vf", ",".join(filters),
161
+ "-an", "-fps_mode", "passthrough", "-f", "null", "-",
162
+ ], capture_output=True, text=True, timeout=600)
163
+ if result.returncode:
164
+ raise RuntimeError(f"scene decode failed: {result.stderr[-2000:]}")
165
+ bases = set(re.findall(r"config in time_base:\s*([0-9]+/[0-9]+)", result.stderr))
166
+ if len(bases) != 1:
167
+ raise ValueError("ambiguous or missing scene filter timebase")
168
+ tb = Fraction(bases.pop())
169
+ if tb <= 0:
170
+ raise ValueError("invalid scene filter timebase")
171
+ values = re.findall(r"\bn:\s*\d+\s+pts:\s*(-?\d+)\s+pts_time:", result.stderr)
172
+ return [int(p) * tb for p in values]
173
+
174
+
175
+ def load_bound_plan(video, plan_path):
176
+ """Only associate with the current cut-render cache: plan, render settings, sources."""
177
+ from cut_contract import _edited_source_meta_path
178
+ try:
179
+ plan = json.loads(Path(plan_path).read_text(encoding="utf-8"))
180
+ meta = json.loads(_edited_source_meta_path(video).read_text(encoding="utf-8"))
181
+ sources = meta["sources"]
182
+ if not isinstance(sources, dict) or not sources:
183
+ raise ValueError("source identities missing")
184
+ if not Path(video).is_file() or Path(video).stat().st_size == 0:
185
+ raise ValueError("rendered media missing")
186
+ if (meta["plan"] != plan["clips"]
187
+ or meta["render_cache"] != edited_source_render_cache_payload()
188
+ or any(file_identity(p) != identity for p, identity in sources.items())):
189
+ raise ValueError("stale media, plan or render settings")
190
+ declared = {c["source_path"] for c in plan["clips"] if "source_path" in c}
191
+ if declared and declared != set(sources):
192
+ raise ValueError("source set mismatch")
193
+ if not declared and len(sources) != 1:
194
+ raise ValueError("ambiguous single source")
195
+ except (OSError, KeyError, ValueError, TypeError) as exc:
196
+ raise ValueError(f"cut plan binding failed: {exc}") from exc
197
+ return plan
198
+
199
+
200
+ def _associate_plan(report, plan, pts, end):
201
+ """Legacy seconds are only a coarse locator, not exact source-frame proof."""
202
+ spans = []
203
+ previous = Fraction(0)
204
+ for clip in plan["clips"]:
205
+ start, stop = Fraction(str(clip["output_start"])), Fraction(str(clip["output_end"]))
206
+ if start != previous or stop <= start:
207
+ raise ValueError("bound cut plan must be contiguous and ordered")
208
+ spans.append((start, stop, clip))
209
+ previous = stop
210
+ tolerance = max(b - a for a, b in zip(pts, [*pts[1:], end]))
211
+ if not spans or abs(previous - end) > tolerance:
212
+ raise ValueError("bound plan duration disagrees with actual frame clock")
213
+ joins = [bisect_left(pts, s[0]) for s in spans[1:]]
214
+ for candidate in report["candidates"]:
215
+ f, t = candidate["frame"], pts[candidate["frame"]]
216
+ if any(abs(f - join) <= 1 for join in joins):
217
+ candidate["origin"] = "EDIT_JOIN_CANDIDATE"
218
+ else:
219
+ for start, stop, clip in spans:
220
+ if start < t < stop:
221
+ candidate["inside_bound_clip"] = clip["clip_id"]
222
+ candidate["source_time_estimate"] = str(Fraction(str(clip["source_start"])) + t - start)
223
+ candidate["mapping_precision"] = "legacy_seconds_estimate"
224
+ # No source scan: motion, exposure, overlay animation and native cuts
225
+ # remain indistinguishable from output scene score alone.
226
+ break
227
+ report["plan_joins"] = joins
228
+
229
+
230
+ def scan_video(video, *, threshold=0.35, plan_path=None, roi=None, **policy):
231
+ video = Path(video).resolve()
232
+ plan = load_bound_plan(video, plan_path) if plan_path is not None else None
233
+ pts, end, origin = probe_frame_clock(video)
234
+ scenes = detect_scene_pts(video, threshold, roi)
235
+ frames_by_pts = {p + origin: i for i, p in enumerate(pts)}
236
+ if any(p not in frames_by_pts for p in scenes):
237
+ raise ValueError("scene candidate does not match a unique decoded frame PTS")
238
+ report = summarize_candidates(pts, end, [frames_by_pts[p] for p in scenes if frames_by_pts[p] > 0], **policy)
239
+ if plan is not None:
240
+ _associate_plan(report, plan, pts, end)
241
+ report.update({
242
+ "schema_version": 1, "artifact": "shot_review", "algorithm": "scene-frame-recall-v1", "scan_complete": True,
243
+ "media": {"path": str(video), "frame_count": len(pts),
244
+ "origin_pts_exact": str(origin), "duration_exact": str(end)},
245
+ "plan_binding": {"path": str(Path(plan_path).resolve())} if plan is not None else None,
246
+ "scene_threshold": threshold,
247
+ "scene_roi": None if roi is None else list(roi),
248
+ "limits": ["scene score is not a confirmed shot or flash-frame defect",
249
+ "source-origin confirmation requires independent source footage review",
250
+ "NO_CANDIDATES is not perceptual approval or listening evidence"],
251
+ })
252
+ return report
253
+
254
+
255
+ def _atomic_json(path, data):
256
+ path = Path(path)
257
+ path.parent.mkdir(parents=True, exist_ok=True)
258
+ temp = None
259
+ try:
260
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
261
+ temp = Path(handle.name)
262
+ json.dump(data, handle, ensure_ascii=False, indent=2)
263
+ handle.flush()
264
+ os.fsync(handle.fileno())
265
+ os.replace(temp, path)
266
+ finally:
267
+ if temp is not None:
268
+ temp.unlink(missing_ok=True)
269
+
270
+
271
+ def _report_target(video, output, plan_path):
272
+ """Unknown existing files are never report targets, even with corrupt source metadata."""
273
+ from cut_contract import _edited_source_meta_path
274
+ protected = {Path(video).resolve(), _edited_source_meta_path(video).resolve()}
275
+ if plan_path is not None:
276
+ protected.add(Path(plan_path).resolve())
277
+ target = Path(output).resolve()
278
+ if target in protected:
279
+ raise ValueError("report must not overwrite media, source, plan or render metadata")
280
+ if Path(output).exists():
281
+ try:
282
+ old = json.loads(Path(output).read_text(encoding="utf-8"))
283
+ if (not isinstance(old, dict) or type(old.get("schema_version")) is not int
284
+ or old["schema_version"] != 1 or old.get("artifact") != "shot_review"):
285
+ raise ValueError("not a shot-review artifact")
286
+ except (OSError, UnicodeError, ValueError) as exc:
287
+ raise ValueError("report must not overwrite an unknown existing file") from exc
288
+ return target
289
+
290
+
291
+ def _protect_declared_sources(video, plan_path, target):
292
+ """Use both plan and metadata declarations, even when they disagree."""
293
+ from cut_contract import _edited_source_meta_path
294
+ if plan_path is None:
295
+ return
296
+ paths, errors = set(), []
297
+ for path, kind in [(Path(plan_path), "plan"), (_edited_source_meta_path(video), "meta")]:
298
+ try:
299
+ data = json.loads(path.read_text(encoding="utf-8"))
300
+ if kind == "plan":
301
+ paths.update(c["source_path"] for c in data["clips"] if "source_path" in c)
302
+ else:
303
+ paths.update(data["sources"])
304
+ except (OSError, ValueError, KeyError, TypeError) as exc:
305
+ errors.append(exc)
306
+ if target in {Path(p).resolve() for p in paths}:
307
+ raise UnsafeReportTarget("report must not overwrite a declared source")
308
+ if errors:
309
+ raise ValueError(f"cannot verify declared source protection: {errors[0]}")
310
+
311
+
312
+ class UnsafeReportTarget(ValueError):
313
+ """Do not write even failure evidence to an input file."""
314
+
315
+
316
+ def write_scan(video, output, **options):
317
+ target = _report_target(video, output, options.get("plan_path"))
318
+ roi = options.get("roi")
319
+ base = {"schema_version": 1, "artifact": "shot_review", "scan_complete": False,
320
+ "normal_speed_review": "NOT_CHECKED",
321
+ "scene_threshold": options.get("threshold", 0.35),
322
+ "scene_roi": None if roi is None else list(roi)}
323
+ try:
324
+ _protect_declared_sources(video, options.get("plan_path"), target)
325
+ _atomic_json(output, {**base, "status": "SCANNING"})
326
+ report = scan_video(video, **options)
327
+ except UnsafeReportTarget:
328
+ raise
329
+ except (OSError, ValueError, RuntimeError, KeyError, TypeError, subprocess.SubprocessError) as exc:
330
+ _atomic_json(output, {**base, "status": "SCAN_FAILED", "error": str(exc)})
331
+ raise
332
+ _atomic_json(output, report)
333
+ return report
334
+
335
+
336
+ def main():
337
+ parser = argparse.ArgumentParser(description=__doc__)
338
+ parser.add_argument("video")
339
+ parser.add_argument("--output", required=True)
340
+ parser.add_argument("--plan", default=None, help="optional current clip_plan_validated.json; stale bindings fail")
341
+ parser.add_argument("--threshold", type=float, default=0.35)
342
+ parser.add_argument("--roi", nargs=4, type=int, metavar=("X", "Y", "WIDTH", "HEIGHT"))
343
+ parser.add_argument("--max-short-frames", type=int, default=None,
344
+ help="explicit frame cap; default derives round(fps * max_short_seconds)")
345
+ parser.add_argument("--max-short-seconds", type=float, default=1.0)
346
+ parser.add_argument("--dense-window-seconds", type=float, default=2.0)
347
+ parser.add_argument("--min-dense-cuts", type=int, default=4)
348
+ args = parser.parse_args()
349
+ report = write_scan(args.video, args.output, threshold=args.threshold, plan_path=args.plan, roi=args.roi,
350
+ max_short_frames=args.max_short_frames, max_short_seconds=args.max_short_seconds,
351
+ dense_window_seconds=args.dense_window_seconds, min_dense_cuts=args.min_dense_cuts)
352
+ print(json.dumps({"status": report["status"], "short_spans": len(report["short_spans"]),
353
+ "dense_windows": len(report["dense_windows"]), "report": args.output}))
354
+
355
+
356
+ if __name__ == "__main__":
357
+ main()