@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
@@ -1,7 +1,6 @@
1
1
  """Self-contained config + utilities for this skill (no cross-skill imports).
2
2
  Merged from the shared core; reads the same env vars as the rest of the bundle."""
3
3
  import json
4
- import hashlib
5
4
  import math
6
5
  import os
7
6
  import re
@@ -25,12 +24,11 @@ MIMO_TOKEN_PLAN_API_URLS = {
25
24
  }
26
25
  DEFAULT_MIMO_MODEL = "mimo-v2.5" # VLM / chat (vision understanding)
27
26
  DEFAULT_MIMO_ASR_MODEL = "mimo-v2.5-asr" # speech-to-text
28
- DEFAULT_MIMO_TTS_MODEL = "mimo-v2.5-tts" # text-to-speech
29
27
 
30
28
 
31
29
  def normalize_api_url(raw_url):
32
30
  """Normalize a MiMo (OpenAI-compatible) base URL or chat/completions endpoint."""
33
- url = (raw_url or DEFAULT_MIMO_API_URL).rstrip("/")
31
+ url = raw_url.rstrip("/")
34
32
  if url.endswith("/chat/completions"):
35
33
  return url
36
34
  return f"{url}/chat/completions"
@@ -118,14 +116,14 @@ _raw_mimo_asr_api_url = (
118
116
  CONFIG = {
119
117
  "api_url": normalize_api_url(_raw_api_url),
120
118
  "api_key": _mimo_api_key,
121
- "api_key_source": "MIMO_API_KEY",
119
+ "api_env_var": "MIMO_API_KEY",
122
120
  "mimo_api_url": normalize_api_url(_raw_api_url),
123
121
  "mimo_api_key": _mimo_api_key,
124
122
  "mimo_video_api_url": normalize_api_url(_raw_mimo_video_api_url),
125
123
  "mimo_video_api_key": _mimo_video_api_key,
126
124
  "mimo_asr_api_url": normalize_api_url(_raw_mimo_asr_api_url),
127
125
  "mimo_asr_api_key": _mimo_asr_api_key,
128
- "mimo_asr_api_key_source": "MIMO_ASR_API_KEY" if os.environ.get("MIMO_ASR_API_KEY") else "MIMO_API_KEY",
126
+ "mimo_asr_env_var": "MIMO_ASR_API_KEY" if os.environ.get("MIMO_ASR_API_KEY") else "MIMO_API_KEY",
129
127
  "mimo_model": os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
130
128
  "mimo_video_model": os.environ.get("MIMO_VIDEO_MODEL") or os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
131
129
  "vlm_model": os.environ.get("MIMO_MODEL", DEFAULT_MIMO_MODEL),
@@ -208,24 +206,6 @@ CONFIG = {
208
206
  SCRIPT_DIR = Path(__file__).parent
209
207
  PROMPTS_DIR = SCRIPT_DIR.parent / "references"
210
208
 
211
- def narration_tempo_budget(tts_rate_offset=0.0, *, config=None):
212
- """Return the canonical tempo budget shared by voiceover and assemble."""
213
- cfg = config or CONFIG
214
- global_speed = max(0.01, float(cfg.get("narration_speed", 1.0) or 1.0))
215
- rate_factor = max(0.01, 1.0 + float(tts_rate_offset or 0.0))
216
- cumulative_max = max(1.0, float(cfg.get("narration_cumulative_tempo_max", 1.35) or 1.35))
217
- hard_max = max(cumulative_max, float(cfg.get("narration_cumulative_tempo_hard_max", 1.40) or 1.40))
218
- legacy_segment_cap = max(1.0, float(cfg.get("tts_segment_tempo_max", 1.20) or 1.20))
219
- segment_tempo_max = max(1.0, min(legacy_segment_cap, cumulative_max / (global_speed * rate_factor)))
220
- return {
221
- "global_narration_speed": global_speed,
222
- "tts_rate_factor": rate_factor,
223
- "cumulative_tempo_max": cumulative_max,
224
- "cumulative_tempo_hard_max": hard_max,
225
- "segment_tempo_max": segment_tempo_max,
226
- "max_raw_duration_factor": global_speed * segment_tempo_max,
227
- }
228
-
229
209
  def log(msg):
230
210
  print(f"[video-recap] {msg}", flush=True)
231
211
 
@@ -250,64 +230,31 @@ def get_video_duration(video_path):
250
230
  "-of", "csv=p=0", str(video_path)]
251
231
  result = run_cmd(cmd)
252
232
  if result.returncode != 0:
253
- return 0.0
233
+ raise RuntimeError(f"ffprobe 无法读取时长 {video_path}: {result.stderr.strip()[-500:]}")
254
234
  try:
255
235
  return float(result.stdout.strip())
256
- except (TypeError, ValueError):
257
- return 0.0
258
-
259
- def stable_json_dumps(value):
260
- """Serialize values deterministically for non-secret cache fingerprints."""
261
- return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
262
-
263
- def stable_hash(value):
264
- """Return an md5 digest for deterministic JSON-serializable values."""
265
- return hashlib.md5(stable_json_dumps(value).encode("utf-8")).hexdigest()
236
+ except ValueError as exc:
237
+ raise RuntimeError(f"ffprobe 时长输出无法解析 {video_path}: {result.stdout.strip()[:100]!r}") from exc
266
238
 
267
- _FILE_FINGERPRINT_MEMO = {}
268
239
 
240
+ def load_background_research(work_dir):
241
+ """Load the agent-authored background_research.json: missing → {}, malformed → raise."""
242
+ path = Path(work_dir) / "background_research.json"
243
+ if not path.exists():
244
+ return {}
245
+ try:
246
+ data = json.loads(path.read_text(encoding="utf-8"))
247
+ except ValueError as exc:
248
+ raise ValueError(f"background_research.json 不是合法 JSON,请修复后重试: {exc}") from exc
249
+ if not isinstance(data, dict):
250
+ raise ValueError("background_research.json 顶层必须是 JSON 对象({...})")
251
+ return data
269
252
 
270
- def _file_identity(path):
271
- """(device, inode, size, mtime_ns) — changes whenever the bytes could have changed."""
253
+ def file_identity(path):
254
+ """{"size", "mtime_ns"} of a file: the cache identity recorded instead of content hashes."""
272
255
  st = os.stat(os.fspath(path))
273
- return (st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
274
-
256
+ return {"size": st.st_size, "mtime_ns": st.st_mtime_ns}
275
257
 
276
- def file_fingerprint(path, chunk_size=1024 * 1024):
277
- """Return a full-content fingerprint for cache-correct identity checks.
278
-
279
- The digest covers CONTENT only — never the path or mtime — so a copied video or
280
- artifact is still recognised as the same asset, while any byte change invalidates
281
- the cache even if timestamps, size, head, or tail bytes are misleading.
282
-
283
- Identity metadata is used ONLY to memoize within a single process. One understanding
284
- run fingerprints the same source video 8-10 times and the whole extracted frame set
285
- 2-3 times; on a 40-minute video at fps=1 that is gigabytes of redundant reads before
286
- any real work starts. A file rewritten in place gets a new (size, mtime_ns) and is
287
- re-hashed, so the memo can never serve a stale digest.
288
- """
289
- key = _file_identity(path)
290
- memoized = _FILE_FINGERPRINT_MEMO.get(key)
291
- if memoized is not None:
292
- return memoized
293
- h = hashlib.sha256()
294
- with open(os.fspath(path), "rb") as f:
295
- for chunk in iter(lambda: f.read(chunk_size), b""):
296
- h.update(chunk)
297
- digest = h.hexdigest()
298
- _FILE_FINGERPRINT_MEMO[key] = digest
299
- return digest
300
- def video_fingerprint(video_path):
301
- """Full video content fingerprint used as the root pipeline asset print."""
302
- return file_fingerprint(video_path)
303
-
304
- def step_cache_key(video_path, step_name, params_fingerprint=""):
305
- """Build a cache key from video content, step name and step parameters."""
306
- params_digest = params_fingerprint
307
- if not isinstance(params_digest, str):
308
- params_digest = stable_hash(params_digest)
309
- payload = f"{video_fingerprint(video_path)}_{step_name}_{params_digest}"
310
- return hashlib.md5(payload.encode("utf-8")).hexdigest()
311
258
 
312
259
  def _retry_after_seconds(value, fallback):
313
260
  """Parse Retry-After seconds or HTTP-date; return fallback on malformed input."""
@@ -342,7 +289,7 @@ def _sanitize_api_error(value, limit=500):
342
289
  def _api_headers(api_provider=None, api_url=None, api_key=None):
343
290
  """Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
344
291
  del api_provider, api_url # MiMo is the only provider; signature kept for call sites
345
- key = CONFIG.get("api_key", "") if api_key is None else api_key
292
+ key = CONFIG["api_key"] if api_key is None else api_key
346
293
  return {
347
294
  "Content-Type": "application/json",
348
295
  "User-Agent": "video-recap/1.0",
@@ -357,7 +304,7 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
357
304
  normalized["max_completion_tokens"] = normalized.pop("max_tokens")
358
305
  model = str(normalized.get("model") or "")
359
306
  if (
360
- CONFIG.get("mimo_disable_thinking", True)
307
+ CONFIG["mimo_disable_thinking"]
361
308
  and not model.endswith(("-tts", "-asr"))
362
309
  and "thinking" not in normalized
363
310
  ):
@@ -369,9 +316,9 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
369
316
  def _mimo_endpoint(kind):
370
317
  """Return per-capability MiMo endpoint settings (video understanding / TTS / ASR)."""
371
318
  by_kind = {
372
- "video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_api_key_source"),
373
- "tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_api_key_source"),
374
- "asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_api_key_source"),
319
+ "video": ("mimo_video_api_url", "mimo_video_api_key", "mimo_video_env_var"),
320
+ "tts": ("mimo_tts_api_url", "mimo_tts_api_key", "mimo_tts_env_var"),
321
+ "asr": ("mimo_asr_api_url", "mimo_asr_api_key", "mimo_asr_env_var"),
375
322
  }
376
323
  if kind not in by_kind:
377
324
  raise ValueError(f"Unsupported MiMo endpoint kind: {kind}")
@@ -379,7 +326,7 @@ def _mimo_endpoint(kind):
379
326
  return {
380
327
  "api_url": CONFIG.get(url_key) or CONFIG.get("mimo_api_url"),
381
328
  "api_key": CONFIG.get(key_key) or CONFIG.get("mimo_api_key"),
382
- "api_key_source": CONFIG.get(src_key, "MIMO_API_KEY"),
329
+ "api_env_var": CONFIG.get(src_key, "MIMO_API_KEY"),
383
330
  }
384
331
 
385
332
  def _call_mimo_endpoint(kind, payload, max_retries=10):
@@ -390,7 +337,7 @@ def _call_mimo_endpoint(kind, payload, max_retries=10):
390
337
  api_provider="mimo",
391
338
  api_url=settings["api_url"],
392
339
  api_key=settings["api_key"],
393
- api_key_source=settings["api_key_source"],
340
+ api_env_var=settings["api_env_var"],
394
341
  )
395
342
 
396
343
  def mimo_video_api_call(payload, max_retries=10):
@@ -401,7 +348,7 @@ def mimo_asr_api_call(payload, max_retries=10):
401
348
  """Call the MiMo speech-recognition (ASR) endpoint."""
402
349
  return _call_mimo_endpoint("asr", payload, max_retries=max_retries)
403
350
 
404
- def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_key_source=None):
351
+ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key=None, api_env_var=None):
405
352
  """调用 OpenAI-compatible API,带重试。
406
353
 
407
354
  长视频理解会发出数百次 VLM/ASR 调用,集群的 429 限流是常态而非错误,所以重试更耐心
@@ -425,7 +372,7 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
425
372
  wait = _retry_after_seconds(retry_after, max(wait, 10))
426
373
  log(f"API 速率限制 (尝试 {attempt+1}/{max_retries}), 等待 {wait}s")
427
374
  elif e.code == 401:
428
- key_name = api_key_source or CONFIG.get("api_key_source", "MIMO_API_KEY")
375
+ key_name = api_env_var or CONFIG["api_env_var"]
429
376
  raise RuntimeError(f"API 认证失败 (401)。请检查 {key_name} 和 API URL 是否匹配。")
430
377
  elif e.code == 403:
431
378
  hint = "API 访问被拒绝 (403)。"
@@ -459,9 +406,6 @@ def api_call(payload, max_retries=8, *, api_provider=None, api_url=None, api_key
459
406
  time.sleep(wait)
460
407
  else:
461
408
  raise RuntimeError(f"API 调用失败 {max_retries} 次: {safe_error}")
462
- # Unreachable for max_retries >= 1; guards against a silent `None` return (and the
463
- # TypeError it would cause at the caller's resp["choices"]) if a caller passes 0.
464
- raise ValueError(f"max_retries must be >= 1, got {max_retries}")
465
409
 
466
410
  def load_prompt(name):
467
411
  """加载 prompt 模板"""
@@ -5,6 +5,7 @@ timing and text checks live here, and every later consumer trusts its result.
5
5
  """
6
6
 
7
7
  import json
8
+ import math
8
9
  from pathlib import Path
9
10
 
10
11
  from lib import CONFIG, log
@@ -32,7 +33,11 @@ def _lint_issue(level, index, code, message, **extra):
32
33
 
33
34
 
34
35
  def _is_number(value):
35
- return isinstance(value, (int, float)) and not isinstance(value, bool)
36
+ return (
37
+ isinstance(value, (int, float))
38
+ and not isinstance(value, bool)
39
+ and math.isfinite(value)
40
+ )
36
41
 
37
42
 
38
43
  def _visual_overlay_issues(index, seg):
@@ -115,17 +120,13 @@ def _clip_span(clip):
115
120
  return clip["start"], clip["end"]
116
121
 
117
122
 
118
- def _clip_matches_for_segment(seg, clip_plan):
123
+ def _clip_matches_for_segment(seg, clip_plan, requested_clip_id):
119
124
  if not clip_plan:
120
125
  return []
121
126
  clips = clip_plan["clips"]
122
127
  midpoint = (seg["start"] + seg["end"]) / 2
123
- if seg.get("source_clip_id") is not None:
124
- try:
125
- requested = int(seg["source_clip_id"])
126
- except (TypeError, ValueError):
127
- return []
128
- clips = [clip for clip in clips if clip.get("clip_id") == requested]
128
+ if requested_clip_id is not None:
129
+ clips = [clip for clip in clips if clip.get("clip_id") == requested_clip_id]
129
130
  return [
130
131
  clip
131
132
  for clip in clips
@@ -196,9 +197,13 @@ def _has_connected_predecessor(narration, idx, start):
196
197
 
197
198
 
198
199
  def lint_narration(
199
- narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
200
+ narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
201
+ require_chronological=False,
200
202
  ):
201
- """Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set."""
203
+ """Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set.
204
+
205
+ Segments are sorted by start for the timing checks; ``require_chronological`` (the
206
+ --preserve-approved-text contract) additionally rejects input that is not already in order."""
202
207
  scenes_analysis = scenes_analysis or []
203
208
  errors = []
204
209
  warnings = []
@@ -215,6 +220,7 @@ def lint_narration(
215
220
  )
216
221
  )
217
222
  else:
223
+ previous_start = None
218
224
  for idx, seg in enumerate(narration):
219
225
  if not isinstance(seg, dict):
220
226
  errors.append(
@@ -230,33 +236,47 @@ def lint_narration(
230
236
  if not _is_number(start) or not _is_number(end):
231
237
  errors.append(
232
238
  _lint_issue(
233
- "error", idx, "invalid_time", "start/end must be numeric"
239
+ "error", idx, "invalid_time", "start/end must be finite numbers"
234
240
  )
235
241
  )
236
242
  continue
237
- text = str(seg.get("narration", "")).strip()
238
- try:
239
- pause = int(seg.get("pause_after_ms", CONFIG["breath_ms"]))
240
- except (TypeError, ValueError):
243
+ if require_chronological and previous_start is not None and start < previous_start:
241
244
  errors.append(
242
245
  _lint_issue(
243
246
  "error",
244
247
  idx,
245
- "invalid_pause",
246
- "pause_after_ms must be an integer number of milliseconds",
248
+ "out_of_order",
249
+ "Segments must be listed in chronological order",
250
+ start=start,
251
+ previous_start=previous_start,
252
+ )
253
+ )
254
+ previous_start = start
255
+ text = seg.get("narration")
256
+ if not isinstance(text, str):
257
+ errors.append(
258
+ _lint_issue(
259
+ "error",
260
+ idx,
261
+ "invalid_narration",
262
+ "narration must be a string",
263
+ start=start,
264
+ end=end,
247
265
  )
248
266
  )
249
267
  continue
250
- if pause < 0:
251
- warnings.append(
268
+ text = text.strip()
269
+ pause = seg.get("pause_after_ms", CONFIG["breath_ms"])
270
+ if not isinstance(pause, int) or isinstance(pause, bool) or pause < 0:
271
+ errors.append(
252
272
  _lint_issue(
253
- "warning",
273
+ "error",
254
274
  idx,
255
- "negative_pause",
256
- "pause_after_ms is negative; default should be used",
257
- pause_after_ms=pause,
275
+ "invalid_pause",
276
+ "pause_after_ms must be a non-negative integer number of milliseconds",
258
277
  )
259
278
  )
279
+ continue
260
280
  errors.extend(_visual_overlay_issues(idx, seg))
261
281
  if end <= start:
262
282
  errors.append(
@@ -388,7 +408,21 @@ def lint_narration(
388
408
  )
389
409
 
390
410
  if mode == "cut":
391
- matches = _clip_matches_for_segment(seg, clip_plan)
411
+ requested_clip_id = seg.get("source_clip_id")
412
+ if requested_clip_id is not None:
413
+ try:
414
+ requested_clip_id = int(requested_clip_id)
415
+ except (TypeError, ValueError):
416
+ errors.append(
417
+ _lint_issue(
418
+ "error",
419
+ idx,
420
+ "invalid_source_clip_id",
421
+ "source_clip_id must be an integer",
422
+ )
423
+ )
424
+ continue
425
+ matches = _clip_matches_for_segment(seg, clip_plan, requested_clip_id)
392
426
  if not matches:
393
427
  errors.append(
394
428
  _lint_issue(
@@ -400,7 +434,7 @@ def lint_narration(
400
434
  end=end,
401
435
  )
402
436
  )
403
- elif len(matches) > 1 and seg.get("source_clip_id") is None:
437
+ elif len(matches) > 1 and requested_clip_id is None:
404
438
  errors.append(
405
439
  _lint_issue(
406
440
  "error",
@@ -426,18 +460,6 @@ def lint_narration(
426
460
  clip_end=round(clip_end, 3),
427
461
  )
428
462
  )
429
- if seg.get("source_clip_id") is not None:
430
- try:
431
- int(seg["source_clip_id"])
432
- except (TypeError, ValueError):
433
- errors.append(
434
- _lint_issue(
435
- "error",
436
- idx,
437
- "invalid_source_clip_id",
438
- "source_clip_id must be an integer",
439
- )
440
- )
441
463
 
442
464
  normalized.append(
443
465
  {"index": idx, "start": start, "end": end, "char_count": char_count}
@@ -620,10 +642,12 @@ def lint_narration(
620
642
 
621
643
 
622
644
  def validate_narration_or_raise(
623
- narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None
645
+ narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
646
+ require_chronological=False,
624
647
  ):
625
648
  report = lint_narration(
626
- narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir
649
+ narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir,
650
+ require_chronological=require_chronological,
627
651
  )
628
652
  if report["errors"]:
629
653
  sample = "; ".join(
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  from pathlib import Path
5
5
 
6
- from lib import CONFIG, stable_hash
6
+ from lib import CONFIG, file_identity
7
7
 
8
8
 
9
9
  def _empty_evidence(mode):
@@ -21,11 +21,11 @@ def _read_json(path):
21
21
 
22
22
 
23
23
  def _output_payload_is_current(payload, work_dir):
24
- plan = _read_json(Path(work_dir) / "clip_plan_validated.json")
24
+ plan_path = Path(work_dir) / "clip_plan_validated.json"
25
25
  return (
26
26
  payload is not None
27
- and plan is not None
28
- and payload["clip_plan_fingerprint"] == stable_hash(plan)
27
+ and plan_path.exists()
28
+ and payload["clip_plan_identity"] == file_identity(plan_path)
29
29
  )
30
30
 
31
31
 
@@ -24,12 +24,7 @@ import subprocess
24
24
  from pathlib import Path
25
25
 
26
26
  from extract import frame_number_for_time, parse_frame_number
27
- from lib import CONFIG, run_cmd, log
28
-
29
- try: # file_fingerprint is the project's content-fingerprint helper; reuse when cheap.
30
- from lib import file_fingerprint
31
- except ImportError: # pragma: no cover - lib always ships it; degrade gracefully if not.
32
- file_fingerprint = None
27
+ from lib import CONFIG, run_cmd, log, get_video_duration
33
28
 
34
29
 
35
30
  # Candidate font files probed (in order) for burning mm:ss labels. The first that exists
@@ -138,14 +133,11 @@ def _scene_anchor_timestamps(scenes, max_tiles):
138
133
  long. Total is capped at max_tiles by evenly subsampling the ordered anchor list, so the
139
134
  sheet stays legible (D2: one bounded contact sheet, not dozens of frame reads).
140
135
  """
141
- long_scene_seconds = float(CONFIG.get("storyboard_long_scene_seconds", 6.0) or 6.0)
136
+ long_scene_seconds = float(CONFIG["storyboard_long_scene_seconds"])
142
137
  anchors = []
143
138
  for scene_id, scene in enumerate(scenes or []):
144
- try:
145
- start = float(scene["start"])
146
- end = float(scene["end"])
147
- except (KeyError, TypeError, ValueError):
148
- continue
139
+ start = float(scene["start"])
140
+ end = float(scene["end"])
149
141
  if end <= start:
150
142
  continue
151
143
  mid = (start + end) / 2.0
@@ -164,15 +156,6 @@ def _scene_anchor_timestamps(scenes, max_tiles):
164
156
  return anchors
165
157
 
166
158
 
167
- def _file_fp(path):
168
- if file_fingerprint is None:
169
- return None
170
- try:
171
- return file_fingerprint(path)
172
- except (OSError, ValueError):
173
- return None
174
-
175
-
176
159
  def _labelled_frame(frame_path, label, font_path, scratch_dir, out_name):
177
160
  """Burn `label` onto a copy of frame_path via drawtext; return the labelled path or None.
178
161
 
@@ -205,7 +188,7 @@ def _tile_pages(frame_paths, columns, out_dir, out_stem, scratch_dir):
205
188
  spill into _001.jpg, _002.jpg… Returns [] on any ffmpeg failure (caller degrades to None).
206
189
  """
207
190
  columns = max(1, int(columns))
208
- rows_per_page = max(1, int(CONFIG.get("storyboard_rows_per_page", 5) or 5))
191
+ rows_per_page = int(CONFIG["storyboard_rows_per_page"])
209
192
  per_page = columns * rows_per_page
210
193
  pages = []
211
194
  total = len(frame_paths)
@@ -234,13 +217,9 @@ def _tile_pages(frame_paths, columns, out_dir, out_stem, scratch_dir):
234
217
  "-vf", f"tile={cols}x{rows}",
235
218
  str(page_path),
236
219
  ]
237
- try:
238
- result = run_cmd(cmd)
239
- except Exception as exc: # noqa: BLE001
240
- log(f"storyboard tile 异常: {exc}")
241
- return []
220
+ result = run_cmd(cmd)
242
221
  if result.returncode != 0 or not page_path.exists():
243
- log(f"storyboard tile 失败: {getattr(result, 'stderr', '')[-300:]}")
222
+ log(f"storyboard tile 失败: {result.stderr[-300:]}")
244
223
  return []
245
224
  pages.append(page_path)
246
225
  return pages
@@ -282,7 +261,7 @@ def _render_storyboard(work_dir, tiles, out_stem):
282
261
 
283
262
  try:
284
263
  pages = _tile_pages(
285
- render_frames, CONFIG.get("storyboard_columns", 6),
264
+ render_frames, CONFIG["storyboard_columns"],
286
265
  storyboard_dir, out_stem, scratch_dir,
287
266
  )
288
267
  finally:
@@ -305,8 +284,8 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
305
284
  if not paths:
306
285
  log("storyboard 跳过 source:frames/ 为空或缺失")
307
286
  return None
308
- max_tiles = int(CONFIG.get("storyboard_max_tiles", 30) or 30)
309
- columns = int(CONFIG.get("storyboard_columns", 6) or 6)
287
+ max_tiles = int(CONFIG["storyboard_max_tiles"])
288
+ columns = int(CONFIG["storyboard_columns"])
310
289
  anchors = _scene_anchor_timestamps(scenes, max_tiles)
311
290
  if not anchors:
312
291
  log("storyboard 跳过 source:无可用场景锚点")
@@ -339,7 +318,6 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
339
318
  "schema_version": 1,
340
319
  "timeline": "source",
341
320
  "video_path": str(video_path),
342
- "video_fingerprint": _file_fp(video_path),
343
321
  "fps": float(fps) if fps else None,
344
322
  "labels_burned": labels_burned,
345
323
  "page_images": [str(p) for p in pages],
@@ -350,9 +328,10 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
350
328
  },
351
329
  "tiles": tiles,
352
330
  }
353
- duration = get_video_duration_safe(video_path)
354
- if duration:
355
- payload["duration"] = round(duration, 3)
331
+ try:
332
+ payload["duration"] = round(get_video_duration(video_path), 3)
333
+ except RuntimeError as exc:
334
+ log(f"storyboard source:无法读取视频时长,sidecar 省略 duration(忽略): {exc}")
356
335
  json_path = work_dir / "storyboard" / "source_storyboard.json"
357
336
  json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
358
337
  log(f"storyboard source: {len(tiles)} tiles → {len(pages)} page(s), labels_burned={labels_burned}")
@@ -362,32 +341,6 @@ def build_source_storyboard(work_dir, video_path, scenes, fps):
362
341
  return None
363
342
 
364
343
 
365
- def get_video_duration_safe(video_path):
366
- """Best-effort source duration via ffprobe; returns None on any failure (advisory)."""
367
- if not shutil.which("ffprobe"):
368
- return None
369
- try:
370
- result = subprocess.run(
371
- ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
372
- "-of", "csv=p=0", str(video_path)],
373
- capture_output=True, text=True, timeout=30,
374
- )
375
- if result.returncode != 0:
376
- return None
377
- return float((result.stdout or "").strip())
378
- except (ValueError, OSError, subprocess.SubprocessError):
379
- return None
380
-
381
-
382
- def _clip_plan_clips(clip_plan_validated):
383
- """Return the clips list from a validated plan (list or {"clips":[...]})."""
384
- if isinstance(clip_plan_validated, dict):
385
- clips = clip_plan_validated.get("clips", [])
386
- else:
387
- clips = clip_plan_validated
388
- return clips if isinstance(clips, list) else []
389
-
390
-
391
344
  def _source_to_output(source_time, clip):
392
345
  """Forward affine source→output map for ONE clip (reimplements cut.py:319 locally).
393
346
 
@@ -416,12 +369,12 @@ def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fp
416
369
  if not paths:
417
370
  log("storyboard 跳过 edited:frames/ 为空或缺失")
418
371
  return None
419
- clips = _clip_plan_clips(clip_plan_validated)
372
+ clips = clip_plan_validated["clips"]
420
373
  if not clips:
421
374
  log("storyboard 跳过 edited:clip_plan_validated 无 clips")
422
375
  return None
423
- columns = int(CONFIG.get("storyboard_columns", 6) or 6)
424
- max_tiles = int(CONFIG.get("storyboard_max_tiles", 30) or 30)
376
+ columns = int(CONFIG["storyboard_columns"])
377
+ max_tiles = int(CONFIG["storyboard_max_tiles"])
425
378
 
426
379
  tiles = []
427
380
  seen_frames = set() # frame-identity dedupe (NOT luma de-dupe; that stays deferred)
@@ -474,7 +427,6 @@ def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fp
474
427
  "timeline": "output",
475
428
  "source_video_path": str(source_video_path),
476
429
  "edited_video_path": str(edited_source) if edited_source.exists() else None,
477
- "clip_plan_fingerprint": _clip_plan_fingerprint(clip_plan_validated),
478
430
  "labels_burned": labels_burned,
479
431
  "page_images": [str(p) for p in pages],
480
432
  "sample_policy": {
@@ -491,17 +443,3 @@ def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fp
491
443
  except Exception as exc: # noqa: BLE001 - advisory: never propagate
492
444
  log(f"storyboard edited 失败(忽略): {exc}")
493
445
  return None
494
-
495
-
496
- def _clip_plan_fingerprint(clip_plan_validated):
497
- """Stable fingerprint of the validated plan that drives the edited tiles."""
498
- try:
499
- return _stable_hash(clip_plan_validated)
500
- except Exception: # noqa: BLE001
501
- return None
502
-
503
-
504
- def _stable_hash(value):
505
- import hashlib
506
- blob = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
507
- return hashlib.md5(blob.encode("utf-8")).hexdigest()