@oh-story/dsh 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/README.md +14 -14
  2. package/lib/client.js +209 -73
  3. package/lib/client.js.map +3 -3
  4. package/lib/drama/manifest.json +248 -168
  5. package/lib/drama/skills/short-drama/SKILL.md +16 -4
  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/creator_markdown_check.py +14 -0
  10. package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
  11. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
  12. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
  13. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +10 -0
  14. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
  15. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
  16. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  17. package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
  18. package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
  19. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
  20. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
  21. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
  22. package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
  23. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
  24. 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
  25. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
  26. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
  27. 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
  28. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
  29. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
  30. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
  31. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
  32. package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
  33. package/lib/drama/skills/short-drama-edit/SKILL.md +65 -44
  34. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
  35. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
  36. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
  37. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
  38. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
  39. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
  40. package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
  41. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
  42. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
  43. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
  44. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
  45. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
  46. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
  47. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
  48. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
  49. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
  50. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +6 -4
  51. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +26 -67
  52. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +31 -56
  53. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +42 -78
  54. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +44 -92
  55. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +14 -16
  56. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1272 -125
  57. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +113 -9
  58. package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
  59. package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
  60. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
  61. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
  62. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
  63. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
  64. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
  65. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
  66. package/lib/drama/skills/short-drama-produce/SKILL.md +5 -1
  67. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
  68. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
  69. package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
  70. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
  71. package/lib/drama/skills/short-drama-review/SKILL.md +4 -5
  72. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
  73. package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
  74. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
  75. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
  76. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +24 -35
  77. package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
  78. package/lib/drama/skills/short-drama-storyboard/SKILL.md +23 -11
  79. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
  80. package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
  81. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
  82. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +139 -34
  83. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +5 -3
  84. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +15 -12
  85. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +29 -0
  86. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
  87. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
  88. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +52 -39
  89. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -21
  90. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +9 -23
  91. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +14 -26
  92. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
  93. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
  94. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
  95. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -2
  96. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
  97. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
  98. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
  99. package/lib/drama/skills/short-drama-write/SKILL.md +10 -7
  100. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
  101. package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
  102. package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
  103. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
  104. package/lib/index.js +346 -98
  105. package/lib/index.js.map +4 -4
  106. package/lib/novel-to-game/README.md +299 -59
  107. package/lib/novel-to-game/README_ZH.md +299 -56
  108. package/lib/novel-to-game/VERSION +1 -1
  109. package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
  110. package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
  111. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
  112. package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
  113. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
  114. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
  115. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
  116. package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
  117. package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
  118. package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
  119. package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
  120. package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
  121. package/lib/novel-to-game/manifest.json +77 -87
  122. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
  123. package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
  124. package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
  125. package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
  126. package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
  127. package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
  128. package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
  129. package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
  130. package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
  131. package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
  132. package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
  133. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
  134. package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
  135. package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
  136. package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
  137. package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
  138. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
  139. package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
  140. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
  141. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
  142. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
  143. package/lib/oh-story/manifest.json +607 -327
  144. package/lib/oh-story/roles/chapter-extractor.md +177 -237
  145. package/lib/oh-story/roles/character-designer.md +1 -2
  146. package/lib/oh-story/roles/consistency-checker.md +15 -13
  147. package/lib/oh-story/roles/narrative-writer.md +36 -38
  148. package/lib/oh-story/roles/story-architect.md +9 -61
  149. package/lib/oh-story/roles/story-explorer.md +20 -125
  150. package/lib/oh-story/roles/story-researcher.md +0 -1
  151. package/lib/oh-story/skills/story/SKILL.md +15 -9
  152. package/lib/oh-story/skills/story/VERSION +1 -1
  153. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
  154. package/lib/oh-story/skills/story/references/author-memory.md +46 -94
  155. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
  156. package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
  157. package/lib/oh-story/skills/story-deslop/SKILL.md +59 -73
  158. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
  159. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +12 -11
  160. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  161. package/lib/oh-story/skills/story-deslop/references/author-memory.md +46 -94
  162. package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
  163. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +22 -5
  164. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
  165. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
  166. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
  167. package/lib/oh-story/skills/story-import/SKILL.md +64 -584
  168. package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
  169. package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
  170. package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
  171. package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
  172. package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
  173. package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
  174. package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
  175. package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
  176. package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
  177. package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
  178. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +98 -92
  179. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
  180. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
  181. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +15 -11
  182. package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
  183. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +132 -27
  184. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +478 -15
  185. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +182 -23
  186. package/lib/oh-story/skills/story-long-analyze/SKILL.md +81 -270
  187. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +252 -0
  188. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +3 -3
  189. package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
  190. package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
  191. package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
  192. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +138 -64
  193. package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  194. package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  195. package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  196. package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  197. package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  198. package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
  199. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +55 -128
  200. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +6 -6
  201. package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  202. package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  203. package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  204. package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  205. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
  206. package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  207. package/lib/oh-story/skills/story-long-scan/SKILL.md +45 -194
  208. package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  209. package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
  210. package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  211. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
  212. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
  213. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
  214. package/lib/oh-story/skills/story-long-write/SKILL.md +49 -81
  215. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
  216. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +12 -11
  217. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +24 -202
  218. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  219. package/lib/oh-story/skills/story-long-write/references/author-memory.md +46 -94
  220. package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
  221. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +7 -11
  222. package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
  223. package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
  224. package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
  225. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -0
  226. package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
  227. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
  228. package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
  229. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  230. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +7 -15
  231. package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
  232. package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
  233. package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
  234. package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
  235. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
  236. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
  237. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
  238. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
  239. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
  240. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
  241. package/lib/oh-story/skills/story-long-write/references/project-files.md +19 -13
  242. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +13 -5
  243. package/lib/oh-story/skills/story-long-write/references/reference-index.md +5 -6
  244. package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
  245. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
  246. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  247. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
  248. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
  249. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +15 -11
  250. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +81 -121
  251. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +41 -139
  252. package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
  253. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +57 -67
  254. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -253
  255. package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
  256. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +6 -4
  257. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +19 -65
  258. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
  259. package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  260. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +226 -25
  261. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
  262. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +132 -27
  263. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
  264. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +7 -4
  265. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +304 -46
  266. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +478 -15
  267. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +182 -23
  268. package/lib/oh-story/skills/story-review/SKILL.md +31 -302
  269. package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
  270. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +12 -11
  271. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
  272. package/lib/oh-story/skills/story-review/references/author-memory.md +46 -94
  273. package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
  274. package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
  275. package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
  276. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +4 -0
  277. package/lib/oh-story/skills/story-review/references/review-quality.md +6 -11
  278. package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
  279. package/lib/oh-story/skills/story-review/references/solo.md +52 -0
  280. package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
  281. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
  282. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +15 -11
  283. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
  284. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
  285. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +478 -15
  286. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +182 -23
  287. package/lib/oh-story/skills/story-setup/SKILL.md +71 -452
  288. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
  289. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +12 -11
  290. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
  291. package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  292. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
  293. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +22 -5
  294. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
  295. package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  296. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
  297. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
  298. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  299. package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
  300. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
  301. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
  302. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
  303. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
  304. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
  305. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
  306. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
  307. package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
  308. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
  309. package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
  310. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
  311. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  312. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
  313. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +19 -65
  314. package/lib/oh-story/skills/story-setup/references/diagnostics.md +4 -4
  315. package/lib/oh-story/skills/story-short-analyze/SKILL.md +56 -110
  316. package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
  317. package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
  318. package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
  319. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
  320. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
  321. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
  322. package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  323. package/lib/oh-story/skills/story-short-analyze/references/stage4-6-reversal-summary.md +611 -0
  324. package/lib/oh-story/skills/story-short-scan/SKILL.md +35 -65
  325. package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
  326. package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  327. package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
  328. package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
  329. package/lib/oh-story/skills/story-short-write/SKILL.md +23 -175
  330. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
  331. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  332. package/lib/oh-story/skills/story-short-write/references/author-memory.md +46 -94
  333. package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
  334. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
  335. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +4 -0
  336. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
  337. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
  338. package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
  339. package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -47
  340. package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
  341. package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
  342. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
  343. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
  344. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
  345. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
  346. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
  347. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
  348. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +21 -13
  349. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +116 -0
  350. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +99 -0
  351. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
  352. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
  353. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
  354. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
  355. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  356. package/lib/types/client/file-activity.d.ts +19 -1
  357. package/lib/types/client/file-activity.d.ts.map +1 -1
  358. package/lib/types/client/index.d.ts.map +1 -1
  359. package/lib/types/client/production-prompts.d.ts +7 -0
  360. package/lib/types/client/production-prompts.d.ts.map +1 -1
  361. package/lib/types/client/production-runtime.d.ts +16 -0
  362. package/lib/types/client/production-runtime.d.ts.map +1 -1
  363. package/lib/types/client/video-studio.d.ts.map +1 -1
  364. package/lib/types/drama-adapters.d.ts +5 -3
  365. package/lib/types/drama-adapters.d.ts.map +1 -1
  366. package/lib/types/native-hooks.d.ts +53 -1
  367. package/lib/types/native-hooks.d.ts.map +1 -1
  368. package/lib/types/role-identity.d.ts +17 -0
  369. package/lib/types/role-identity.d.ts.map +1 -0
  370. package/lib/types/role-tool.d.ts.map +1 -1
  371. package/lib/types/skill-provider.d.ts.map +1 -1
  372. package/lib/types/video-project.d.ts.map +1 -1
  373. package/lib/types/workspace-route.d.ts.map +1 -1
  374. package/lib/video-recap/CHANGELOG.md +60 -1
  375. package/lib/video-recap/README.en.md +348 -137
  376. package/lib/video-recap/README.md +340 -136
  377. package/lib/video-recap/manifest.json +406 -251
  378. package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
  379. package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
  380. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  381. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
  382. package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
  383. package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
  384. package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
  385. package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
  386. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
  387. package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  388. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  389. package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  390. package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  391. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  392. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  393. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
  394. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
  395. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
  396. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
  397. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
  398. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
  399. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
  400. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
  401. package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  402. package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
  403. package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
  404. package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
  405. package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
  406. package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
  407. package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
  408. package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
  409. package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
  410. package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
  411. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
  412. package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
  413. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
  414. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
  415. package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
  416. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
  417. package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  418. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
  419. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
  420. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
  421. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  422. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
  423. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
  424. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
  425. package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
  426. package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
  427. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
  428. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
  429. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
  430. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
  431. package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
  432. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
  433. package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
  434. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
  435. package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
  436. package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
  437. package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
  438. package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
  439. package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
  440. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
  441. package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
  442. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
  443. package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
  444. package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
  445. package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
  446. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
  447. package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
  448. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
  449. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
  450. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
  451. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
  452. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
  453. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
  454. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
  455. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
  456. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
  457. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
  458. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
  459. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
  460. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
  461. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
  462. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
  463. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
  464. package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
  465. package/lib/video-recap/skills/video-script/SKILL.md +16 -13
  466. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
  467. package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
  468. package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
  469. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
  470. package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
  471. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
  472. package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
  473. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
  474. package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
  475. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
  476. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
  477. package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
  478. package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
  479. package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
  480. package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
  481. package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
  482. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  483. package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
  484. package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  485. package/lib/video-recap/skills/{video-script/scripts/agent_brief.py → video-understanding/scripts/briefing/builder.py} +38 -35
  486. package/lib/video-recap/skills/video-understanding/scripts/{brief_context.py → briefing/context.py} +4 -35
  487. package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
  488. package/lib/video-recap/skills/{video-script/scripts/brief_timeline.py → video-understanding/scripts/briefing/timeline.py} +5 -6
  489. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
  490. package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
  491. package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
  492. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
  493. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
  494. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
  495. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
  496. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
  497. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
  498. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
  499. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
  500. package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
  501. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
  502. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  503. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
  504. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
  505. package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  506. package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  507. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
  508. package/package.json +45 -45
  509. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
  510. package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
  511. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
  512. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -530
  513. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -559
  514. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
  515. package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
  516. package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +0 -678
  517. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
  518. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
  519. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
  520. package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
  521. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
  522. package/lib/video-recap/skills/video-script/scripts/brief_context.py +0 -348
  523. package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
  524. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +0 -399
  525. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
  526. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +0 -327
  527. /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
  528. /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
  529. /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
@@ -0,0 +1,187 @@
1
+ """Probe and verify an adopted AAC stream without decoding or rewriting it."""
2
+
3
+ import json
4
+ from fractions import Fraction
5
+ from pathlib import Path
6
+
7
+ from lib import run_cmd
8
+
9
+
10
+ def _fraction(value):
11
+ return Fraction(str(value))
12
+
13
+
14
+ def _packet_span(audio):
15
+ """The stream duration proved by its packets: last packet end minus first packet start."""
16
+ packets = audio["packets"]
17
+ if not packets or packets[0]["pts"] is None or packets[-1]["pts"] is None \
18
+ or packets[-1]["duration"] is None:
19
+ return None
20
+ return Fraction(packets[-1]["pts"]) + Fraction(packets[-1]["duration"]) \
21
+ - Fraction(packets[0]["pts"])
22
+
23
+
24
+ def _rational_ticks(value, time_base):
25
+ if value in (None, "N/A"):
26
+ return None
27
+ value = Fraction(int(value)) * Fraction(time_base)
28
+ return f"{value.numerator}/{value.denominator}"
29
+
30
+
31
+ def _probe(path, *args):
32
+ result = run_cmd([
33
+ "ffprobe", "-v", "error", *args, "-of", "json", str(path),
34
+ ])
35
+ if result.returncode != 0:
36
+ raise RuntimeError(f"无法探测媒体 {path}: {result.stderr.strip()}")
37
+ try:
38
+ return json.loads(result.stdout)
39
+ except (TypeError, ValueError) as exc:
40
+ raise RuntimeError(f"ffprobe 返回无效 JSON: {path}") from exc
41
+
42
+
43
+ def probe_audio_packets(path, audio_stream_index):
44
+ """Return packet sizes and rational timestamps for one audio stream.
45
+
46
+ ``audio_stream_index`` is the zero-based audio-stream ordinal accepted by
47
+ ffmpeg's ``0:a:N`` selector, not the file-wide absolute stream index.
48
+ """
49
+ payload = _probe(
50
+ path,
51
+ "-select_streams", f"a:{audio_stream_index}",
52
+ "-show_streams", "-show_packets",
53
+ "-show_entries",
54
+ "stream=index,codec_name,time_base,start_pts,start_time,duration_ts,duration,sample_rate,channels,"
55
+ "channel_layout:packet=pts,dts,duration,size,side_data_list",
56
+ )
57
+ streams = payload.get("streams", [])
58
+ if len(streams) != 1:
59
+ raise RuntimeError(f"找不到音频流 a:{audio_stream_index}: {Path(path)}")
60
+ stream = streams[0]
61
+ time_base = stream.get("time_base")
62
+ if not time_base:
63
+ raise RuntimeError(f"音频流 a:{audio_stream_index} 缺少 time_base")
64
+ codec = stream.get("codec_name")
65
+ sample_rate = int(stream["sample_rate"]) if stream.get("sample_rate") else None
66
+ channels = int(stream["channels"]) if stream.get("channels") else None
67
+ channel_layout = stream.get("channel_layout")
68
+ decoder = {
69
+ "codec": codec,
70
+ "sample_rate": sample_rate,
71
+ "channels": channels,
72
+ "channel_layout": channel_layout,
73
+ }
74
+ packets = []
75
+ for packet in payload.get("packets", []):
76
+ if packet.get("size") is None:
77
+ raise RuntimeError(f"音频流 a:{audio_stream_index} 的 packet 缺少 size")
78
+ packets.append({
79
+ "size": int(packet["size"]),
80
+ "pts": _rational_ticks(packet.get("pts"), time_base),
81
+ "dts": _rational_ticks(packet.get("dts"), time_base),
82
+ "duration": _rational_ticks(packet.get("duration"), time_base),
83
+ "side_data_list": packet.get("side_data_list", []),
84
+ })
85
+ return {
86
+ "selected_audio_stream_index": audio_stream_index,
87
+ "absolute_stream_index": int(stream["index"]),
88
+ "codec": codec,
89
+ "time_base": time_base,
90
+ "start_time": stream.get("start_time"),
91
+ "duration": stream.get("duration"),
92
+ "sample_rate": sample_rate,
93
+ "channels": channels,
94
+ "channel_layout": channel_layout,
95
+ "decoder": decoder,
96
+ "packet_count": len(packets),
97
+ "payload_bytes": sum(packet["size"] for packet in packets),
98
+ "packets": packets,
99
+ }
100
+
101
+
102
+ def _picture_interval(path):
103
+ payload = _probe(
104
+ path,
105
+ "-select_streams", "v:0", "-show_streams",
106
+ "-show_entries", "stream=time_base,start_pts,start_time,duration_ts,duration,avg_frame_rate",
107
+ )
108
+ streams = payload.get("streams", [])
109
+ if len(streams) != 1:
110
+ raise RuntimeError(f"找不到主画面流 v:0: {Path(path)}")
111
+ stream = streams[0]
112
+ start = _fraction(stream.get("start_time", "0"))
113
+ if stream.get("duration") in (None, "N/A"):
114
+ raise RuntimeError("主画面流缺少可验证时长")
115
+ duration = _fraction(stream["duration"])
116
+ frame_rate = Fraction(stream.get("avg_frame_rate", "0/1"))
117
+ frame = Fraction(1, 1000) if frame_rate <= 0 else 1 / frame_rate
118
+ return start, duration, frame
119
+
120
+
121
+ def validate_adopted_source(path, audio_stream_index):
122
+ """Fail unless the selected input is copyable AAC spanning the picture interval."""
123
+ audio = probe_audio_packets(path, audio_stream_index)
124
+ if audio["codec"] != "aac":
125
+ raise RuntimeError(
126
+ f"adopted-packet-copy 当前只支持 MP4 AAC stream-copy;"
127
+ f"a:{audio_stream_index} 是 {audio['codec'] or 'unknown'}"
128
+ )
129
+ if not audio["packets"]:
130
+ raise RuntimeError(f"音频流 a:{audio_stream_index} 没有 packet,不能采用")
131
+ picture_start, picture_duration, frame_tolerance = _picture_interval(path)
132
+ if audio["start_time"] in (None, "N/A") or audio["duration"] in (None, "N/A"):
133
+ raise RuntimeError(f"音频流 a:{audio_stream_index} 缺少可验证起止时间")
134
+ audio_start = _fraction(audio["start_time"])
135
+ audio_duration = _fraction(audio["duration"])
136
+ packet_durations = [
137
+ _fraction(packet["duration"])
138
+ for packet in audio["packets"] if packet["duration"] is not None
139
+ ]
140
+ tolerance = max([frame_tolerance, Fraction(1, 1000), *packet_durations])
141
+ if (
142
+ abs(audio_start - picture_start) > tolerance
143
+ or abs(audio_duration - picture_duration) > tolerance
144
+ ):
145
+ raise RuntimeError(
146
+ "采用音频与画面时长/起点不兼容: "
147
+ f"picture={float(picture_start):.6f}+{float(picture_duration):.6f}s, "
148
+ f"audio={float(audio_start):.6f}+{float(audio_duration):.6f}s"
149
+ )
150
+ return audio
151
+
152
+
153
+ def verify_adopted_audio(input_path, output_path, input_stream_index, output_stream_index=0):
154
+ """Probe the rendered output and check packet count, payload bytes, duration and timing."""
155
+ expected = probe_audio_packets(input_path, input_stream_index)
156
+ actual = probe_audio_packets(output_path, output_stream_index)
157
+ if expected["decoder"] != actual["decoder"]:
158
+ raise RuntimeError("采用音频 decoder 参数已改变")
159
+ if expected["packet_count"] != actual["packet_count"]:
160
+ raise RuntimeError(
161
+ f"采用音频 packet count 已改变: {expected['packet_count']} -> {actual['packet_count']}"
162
+ )
163
+ if expected["payload_bytes"] != actual["payload_bytes"]:
164
+ raise RuntimeError("采用音频 packet payload 总字节数不一致")
165
+ if _packet_span(expected) != _packet_span(actual):
166
+ raise RuntimeError("采用音频流时长已改变")
167
+ expected_packet_core = [
168
+ {key: value for key, value in packet.items() if key != "side_data_list"}
169
+ for packet in expected["packets"]
170
+ ]
171
+ actual_packet_core = [
172
+ {key: value for key, value in packet.items() if key != "side_data_list"}
173
+ for packet in actual["packets"]
174
+ ]
175
+ if expected_packet_core != actual_packet_core:
176
+ raise RuntimeError("采用音频 packet PTS/DTS/duration/size 不一致")
177
+ if [packet["side_data_list"] for packet in expected["packets"]] != [
178
+ packet["side_data_list"] for packet in actual["packets"]
179
+ ]:
180
+ raise RuntimeError("采用音频 packet side data 已改变")
181
+ return {
182
+ "verified": True,
183
+ "selected_audio_stream_index": input_stream_index,
184
+ "output_audio_stream_index": output_stream_index,
185
+ "input": expected,
186
+ "output": actual,
187
+ }
@@ -0,0 +1,323 @@
1
+ """Record which narration inputs an assembly consumed: adoption, snapshot, placement, output."""
2
+
3
+ import json
4
+ import math
5
+ from pathlib import Path
6
+ import shutil
7
+ import subprocess
8
+
9
+ from adoption.frozen_audio import probe_audio_packets
10
+ from adoption.strict_inputs import (
11
+ read_json_bytes, require_fields, require_local_path, without_digests, write_json_atomic,
12
+ )
13
+
14
+
15
+ ARTIFACT = "narration_input_binding"
16
+ FILENAME = "narration_input_binding.json"
17
+ IDENTITY_STATUSES = frozenset({"UNADOPTED", "BOUND_TO_ADOPTION"})
18
+ # The conservative default an adoption gets when it declares nothing stronger.
19
+ TEMPO_POLICY = {
20
+ "global_atempo": 1.0,
21
+ "bounded_segment_fit": False,
22
+ "segment_tempo_max": 1.0,
23
+ "cumulative_tempo_max": 1.0,
24
+ "cumulative_tempo_hard_max": 1.0,
25
+ }
26
+ TEMPO_NUMBERS = ("global_atempo", "segment_tempo_max", "cumulative_tempo_max",
27
+ "cumulative_tempo_hard_max")
28
+
29
+
30
+ def validate_tempo_policy(value):
31
+ """Accept any adoption-declared tempo policy whose shape and bounds hold.
32
+
33
+ The adoption, not this module, decides how fast its own narration may be
34
+ played; the module only refuses policies that are malformed or that would
35
+ let a segment exceed the cumulative hard ceiling the policy itself declares.
36
+ """
37
+ require_fields(value, sorted(TEMPO_POLICY), "tempo_policy")
38
+ policy = {}
39
+ for key in TEMPO_NUMBERS:
40
+ number = value[key]
41
+ if type(number) not in (int, float) or not math.isfinite(number):
42
+ raise ValueError(f"tempo_policy {key} must be a finite number")
43
+ policy[key] = float(number)
44
+ if type(value["bounded_segment_fit"]) is not bool:
45
+ raise ValueError("tempo_policy bounded_segment_fit must be a boolean")
46
+ policy["bounded_segment_fit"] = value["bounded_segment_fit"]
47
+ if policy["cumulative_tempo_max"] < 1.0:
48
+ raise ValueError("tempo_policy cumulative_tempo_max must be at least 1.0")
49
+ if policy["cumulative_tempo_hard_max"] < policy["cumulative_tempo_max"]:
50
+ raise ValueError("tempo_policy cumulative_tempo_hard_max must not be below "
51
+ "cumulative_tempo_max")
52
+ if policy["segment_tempo_max"] < 1.0:
53
+ raise ValueError("tempo_policy segment_tempo_max must be at least 1.0")
54
+ if not 0 < policy["global_atempo"] <= policy["cumulative_tempo_hard_max"]:
55
+ raise ValueError("tempo_policy global_atempo must be positive and within "
56
+ "cumulative_tempo_hard_max")
57
+ return policy
58
+
59
+
60
+ def load_adoption(path, *, tts_meta_path, tts_segments):
61
+ """Load a strict v1 adoption and check it against the current tts_meta segments."""
62
+ if tts_meta_path is None:
63
+ raise ValueError("narration adoption requires explicit tts_meta_path")
64
+ adoption_path, _, adoption = read_json_bytes(path, "narration adoption")
65
+ adoption = without_digests(adoption, "narration adoption")
66
+ require_fields(adoption, ["artifact", "schema_version", "segments", "tempo_policy"],
67
+ "narration adoption")
68
+ if adoption["artifact"] != "narration_adoption" or type(adoption["schema_version"]) is not int \
69
+ or adoption["schema_version"] != 1:
70
+ raise ValueError("unsupported narration_adoption schema")
71
+ tts_meta_path, _, tts_meta = read_json_bytes(tts_meta_path, "tts_meta")
72
+ if not isinstance(tts_meta, dict) or not isinstance(tts_meta.get("segments"), list):
73
+ raise ValueError("tts_meta requires a segments list")
74
+ if tts_meta["segments"] != tts_segments:
75
+ raise ValueError("in-memory narration segments differ from bound tts_meta")
76
+ tempo_policy = validate_tempo_policy(adoption["tempo_policy"])
77
+ if not isinstance(adoption["segments"], list) or len(adoption["segments"]) != len(tts_segments):
78
+ raise ValueError("adoption segments must exactly cover tts_meta segments")
79
+ normalized_segments = []
80
+ for adopted, actual in zip(adoption["segments"], tts_segments):
81
+ adopted = without_digests(adopted, "adoption segment")
82
+ require_fields(adopted, ["index", "spoken_text", "requested_provider", "requested_voice"],
83
+ "adoption segment")
84
+ if type(adopted["index"]) is not int or adopted["index"] != actual.get("index"):
85
+ raise ValueError("adoption segment index/order differs from tts_meta")
86
+ spoken = actual.get("spoken_text", actual.get("narration"))
87
+ if not isinstance(adopted["spoken_text"], str) or adopted["spoken_text"] != spoken:
88
+ raise ValueError("adoption spoken_text differs from tts_meta")
89
+ for key in ("requested_provider", "requested_voice"):
90
+ if not isinstance(adopted[key], str) or not adopted[key]:
91
+ raise ValueError(f"adoption {key} must be a non-empty string")
92
+ normalized_segments.append(dict(adopted))
93
+ return {
94
+ "path": str(adoption_path),
95
+ "tts_meta": {"path": str(tts_meta_path)},
96
+ "segments": normalized_segments, "tempo_policy": tempo_policy,
97
+ }
98
+
99
+
100
+ def _copy_snapshot(source, destination):
101
+ """Copy seam kept small so tests can inject a post-preflight source mutation."""
102
+ shutil.copyfile(source, destination)
103
+
104
+
105
+ def prepare_binding(tts_segments, work_dir, *, narration_adoption_path=None,
106
+ tts_meta_path=None):
107
+ """Validate first, then snapshot the adopted narration inputs into work_dir."""
108
+ if not isinstance(tts_segments, list):
109
+ raise ValueError("tts_segments must be a list")
110
+ adoption = (
111
+ load_adoption(narration_adoption_path, tts_meta_path=tts_meta_path,
112
+ tts_segments=tts_segments)
113
+ if narration_adoption_path is not None else None
114
+ )
115
+ if not adoption:
116
+ originals = [
117
+ {"index": segment["index"], "source": Path(segment["audio_path"]).resolve(),
118
+ "spoken_text": segment["spoken_text"]}
119
+ for segment in tts_segments
120
+ ]
121
+ return {
122
+ "identity_status": "UNADOPTED", "adoption": None, "tempo_policy": None,
123
+ "originals": originals, "active": False, "segments": [],
124
+ }
125
+ originals = []
126
+ seen_indices = set()
127
+ for position, segment in enumerate(tts_segments):
128
+ if not isinstance(segment, dict) or type(segment.get("index")) is not int:
129
+ raise ValueError("each narration segment requires an integer index")
130
+ if segment["index"] in seen_indices:
131
+ raise ValueError("narration segment indices must be unique")
132
+ seen_indices.add(segment["index"])
133
+ adopted = adoption["segments"][position]
134
+ source = require_local_path(segment["audio_path"], "narration audio")
135
+ originals.append({
136
+ "index": segment["index"], "source": source,
137
+ "spoken_text": segment["spoken_text"],
138
+ "requested_provider": adopted["requested_provider"],
139
+ "requested_voice": adopted["requested_voice"],
140
+ })
141
+ context = {
142
+ "identity_status": "BOUND_TO_ADOPTION", "adoption": adoption,
143
+ "tempo_policy": adoption["tempo_policy"],
144
+ "originals": originals, "active": True, "segments": [], "sealed": None,
145
+ }
146
+ snapshot_dir = Path(work_dir).resolve() / ".narration_input_snapshots"
147
+ snapshot_dir.mkdir(parents=True, exist_ok=False)
148
+ for position, (segment, original) in enumerate(zip(tts_segments, originals)):
149
+ suffix = original["source"].suffix or ".audio"
150
+ snapshot = snapshot_dir / f"segment_{position:04d}_{segment['index']}{suffix}"
151
+ _copy_snapshot(original["source"], snapshot)
152
+ segment["audio_path"] = str(snapshot)
153
+ segment["narration_input_original"] = {"path": str(original["source"])}
154
+ segment["narration_input_snapshot"] = {"path": str(snapshot)}
155
+ context["segments"].append({
156
+ "index": original["index"], "spoken_text": original["spoken_text"],
157
+ "requested_provider": original["requested_provider"],
158
+ "requested_voice": original["requested_voice"],
159
+ "original": dict(segment["narration_input_original"]),
160
+ "snapshot": dict(segment["narration_input_snapshot"]),
161
+ })
162
+ return context
163
+
164
+
165
+ def _pcm(path):
166
+ result = subprocess.run(
167
+ ["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_streams",
168
+ "-show_entries", "stream=codec_name,sample_fmt,sample_rate,channels,channel_layout",
169
+ "-of", "json", str(path)], capture_output=True, text=True, timeout=600,
170
+ )
171
+ if result.returncode:
172
+ raise ValueError(f"audio probe failed: {result.stderr.strip()}")
173
+ streams = json.loads(result.stdout).get("streams", [])
174
+ if len(streams) != 1:
175
+ raise ValueError("expected exactly one audio stream")
176
+ stream = streams[0]
177
+ return {key: stream.get(key) for key in (
178
+ "codec_name", "sample_fmt", "sample_rate", "channels", "channel_layout"
179
+ )}
180
+
181
+
182
+ def _asset(path, *, pcm=False):
183
+ path = require_local_path(path, "binding asset")
184
+ value = {"path": str(path)}
185
+ if pcm:
186
+ value["pcm"] = _pcm(path)
187
+ return value
188
+
189
+
190
+ def seal_render_inputs(context, tts_segments, narration_wav):
191
+ """Record every derived audio file that the final FFmpeg command will consume."""
192
+ if not context.get("active"):
193
+ return None
194
+ by_index = {segment["index"]: segment for segment in tts_segments}
195
+ sealed_segments = []
196
+ for item in context["segments"]:
197
+ segment = by_index[item["index"]]
198
+ placed = segment.get("placed_audio_path")
199
+ if not placed:
200
+ raise RuntimeError("active narration binding has no complete placed audio")
201
+ conversion = segment.get("narration_conversion_path")
202
+ conversion_asset = (
203
+ {"applied": True, **_asset(conversion, pcm=True)}
204
+ if conversion else {"applied": False}
205
+ )
206
+ sealed_segments.append({
207
+ "index": item["index"],
208
+ "conversion": conversion_asset,
209
+ "placed": _asset(placed, pcm=True),
210
+ })
211
+ context["sealed"] = {
212
+ "segments": sealed_segments,
213
+ "narration_bus": _asset(narration_wav, pcm=True),
214
+ }
215
+ return context["sealed"]
216
+
217
+
218
+ def _active_report(context, rendered_output, final_output):
219
+ if not context.get("sealed"):
220
+ raise RuntimeError("active narration inputs must be sealed before finalization")
221
+ sealed = {item["index"]: item for item in context["sealed"]["segments"]}
222
+ segments = []
223
+ for item in context["segments"]:
224
+ segments.append({
225
+ **item,
226
+ "original": _asset(item["original"]["path"], pcm=True),
227
+ "snapshot": _asset(item["snapshot"]["path"], pcm=True),
228
+ "conversion": sealed[item["index"]]["conversion"],
229
+ "placed": sealed[item["index"]]["placed"],
230
+ })
231
+ rendered_path = require_local_path(rendered_output, "rendered output")
232
+ final_audio = probe_audio_packets(rendered_path, 0)
233
+ return {
234
+ "artifact": ARTIFACT, "schema_version": 1, "status": "FINALIZED",
235
+ "identity_status": context["identity_status"], "adoption": context["adoption"],
236
+ "segments": segments, "narration_bus": context["sealed"]["narration_bus"],
237
+ "final_output": {
238
+ "path": str(Path(final_output).resolve()),
239
+ "audio_stream": {
240
+ "decoder": final_audio["decoder"], "packet_count": final_audio["packet_count"],
241
+ "payload_bytes": final_audio["payload_bytes"],
242
+ "start_time": final_audio["start_time"], "duration": final_audio["duration"],
243
+ },
244
+ },
245
+ "voice_authentication": "NOT_CHECKED", "direct_listening": "NOT_CHECKED",
246
+ }
247
+
248
+
249
+ def finalize_binding(context, tts_segments, narration_wav, final_output, *, rendered_output=None):
250
+ """Write ``narration_input_binding.json`` beside ``narration_wav`` and return it.
251
+
252
+ ``rendered_output`` is the candidate file to probe when the published
253
+ ``final_output`` path does not exist yet; it defaults to ``final_output``.
254
+ """
255
+ del tts_segments # already sealed; the record describes what was consumed
256
+ if not context.get("active"):
257
+ report = {
258
+ "artifact": ARTIFACT, "schema_version": 1, "status": "FINALIZED",
259
+ "identity_status": "UNADOPTED", "adoption": None,
260
+ "segments": [
261
+ {
262
+ "index": original["index"], "spoken_text": original["spoken_text"],
263
+ "requested_provider": None, "requested_voice": None,
264
+ "original": {"path": str(original["source"])},
265
+ "snapshot": None, "conversion": {"applied": False},
266
+ "placed": None,
267
+ }
268
+ for original in context["originals"]
269
+ ],
270
+ "narration_bus": ({"path": str(Path(narration_wav).resolve())}
271
+ if Path(narration_wav).is_file() else None),
272
+ "final_output": ({"path": str(Path(final_output).resolve())}
273
+ if Path(final_output).is_file() else None),
274
+ "voice_authentication": "NOT_CHECKED", "direct_listening": "NOT_CHECKED",
275
+ }
276
+ else:
277
+ report = _active_report(
278
+ context, final_output if rendered_output is None else rendered_output, final_output
279
+ )
280
+ write_json_atomic(Path(narration_wav).resolve().parent / FILENAME, report)
281
+ return report
282
+
283
+
284
+ def record_of(report, path):
285
+ """The manifest/QC summary of one finalized binding report stored at ``path``."""
286
+ return {"path": str(Path(path).resolve()),
287
+ "identity_status": report["identity_status"],
288
+ "tempo_policy": (
289
+ report["adoption"]["tempo_policy"]
290
+ if report["identity_status"] == "BOUND_TO_ADOPTION" else None
291
+ )}
292
+
293
+
294
+ def binding_record(work_dir):
295
+ """The finalized binding's path, identity status and tempo policy; None when absent/invalid."""
296
+ path = Path(work_dir) / FILENAME
297
+ if not path.is_file():
298
+ return None
299
+ try:
300
+ value = json.loads(path.read_text(encoding="utf-8"))
301
+ except (UnicodeDecodeError, json.JSONDecodeError):
302
+ return None
303
+ if (
304
+ not isinstance(value, dict)
305
+ or value.get("artifact") != ARTIFACT
306
+ or type(value.get("schema_version")) is not int
307
+ or value.get("schema_version") != 1
308
+ or value.get("status") != "FINALIZED"
309
+ or value.get("identity_status") not in IDENTITY_STATUSES
310
+ ):
311
+ return None
312
+ if value["identity_status"] == "BOUND_TO_ADOPTION":
313
+ adoption = value.get("adoption")
314
+ if not isinstance(adoption, dict):
315
+ return None
316
+ try:
317
+ validate_tempo_policy(adoption.get("tempo_policy"))
318
+ except ValueError:
319
+ return None
320
+ final_output = value.get("final_output")
321
+ if not isinstance(final_output, dict) or not Path(str(final_output.get("path", ""))).is_file():
322
+ return None
323
+ return record_of(value, path)
@@ -0,0 +1,109 @@
1
+ """Shared strict-input helpers for the explicit-input assemble modules.
2
+
3
+ Every helper here fails closed with ``ValueError`` on malformed input and never
4
+ guesses: a path must be a local file, a JSON document must parse, a rational must
5
+ be canonical ``N/D``. ``run_logged`` and ``probe_json`` wrap the external tools so
6
+ each caller logs the same evidence.
7
+ """
8
+
9
+ from fractions import Fraction
10
+ import json
11
+ import math
12
+ from pathlib import Path
13
+ import subprocess
14
+
15
+
16
+ def require_fields(value, required, label):
17
+ if not isinstance(value, dict) or set(value) != set(required):
18
+ raise ValueError(f"{label} requires exactly fields {required}")
19
+
20
+
21
+ def without_digests(value, label):
22
+ """Drop the ``sha256`` / ``*_sha256`` keys older caller JSON declared; they are ignored."""
23
+ if not isinstance(value, dict):
24
+ raise ValueError(f"{label} must be an object")
25
+ return {key: item for key, item in value.items()
26
+ if key != "sha256" and not key.endswith("_sha256")}
27
+
28
+
29
+ def require_integer(value, label, minimum=0):
30
+ if type(value) is not int or value < minimum:
31
+ raise ValueError(f"{label} must be an integer >= {minimum}")
32
+ return value
33
+
34
+
35
+ def require_number(value, label, minimum, maximum):
36
+ if type(value) not in (int, float) or not math.isfinite(value) \
37
+ or not minimum <= value <= maximum:
38
+ raise ValueError(f"{label} must be finite in [{minimum},{maximum}]")
39
+ return float(value)
40
+
41
+
42
+ def require_local_path(path, label):
43
+ if not isinstance(path, (str, Path)) or not str(path) or "://" in str(path):
44
+ raise ValueError(f"{label} requires a local path")
45
+ resolved = Path(path).resolve()
46
+ if not resolved.is_file():
47
+ raise ValueError(f"{label} is missing: {resolved}")
48
+ return resolved
49
+
50
+
51
+ def require_declared_path(value, label):
52
+ """Resolve a caller ``{"path": ...}`` declaration to an existing local file."""
53
+ if not isinstance(value, dict):
54
+ raise ValueError(f"{label} must be an object with a path")
55
+ return require_local_path(value.get("path"), label)
56
+
57
+
58
+ def read_json_bytes(path, label):
59
+ """Return (resolved_path, raw_bytes, parsed) for one local JSON document."""
60
+ resolved = require_local_path(path, label)
61
+ raw = resolved.read_bytes()
62
+ try:
63
+ return resolved, raw, json.loads(raw)
64
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
65
+ raise ValueError(f"{label} is not valid JSON") from exc
66
+
67
+
68
+ def write_json_atomic(path, value):
69
+ path = Path(path)
70
+ temporary = path.with_suffix(".writing.json")
71
+ temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
72
+ temporary.replace(path)
73
+
74
+
75
+ def canonical_fraction(value, label):
76
+ if not isinstance(value, str):
77
+ raise ValueError(f"{label} must be a canonical rational string")
78
+ try:
79
+ result = Fraction(value)
80
+ except (ValueError, ZeroDivisionError) as exc:
81
+ raise ValueError(f"invalid {label}") from exc
82
+ if result <= 0 or value != f"{result.numerator}/{result.denominator}":
83
+ raise ValueError(f"{label} must be a positive canonical N/D rational")
84
+ return result
85
+
86
+
87
+ def probe_json(path, *ffprobe_args):
88
+ result = subprocess.run(
89
+ ["ffprobe", "-v", "error", *ffprobe_args, "-of", "json", str(path)],
90
+ capture_output=True, text=True, timeout=600,
91
+ )
92
+ if result.returncode or result.stderr.strip():
93
+ raise ValueError(f"media probe failed for {path}: {result.stderr.strip()}")
94
+ try:
95
+ return json.loads(result.stdout)
96
+ except json.JSONDecodeError as exc:
97
+ raise ValueError(f"invalid media probe JSON for {path}") from exc
98
+
99
+
100
+ def run_logged(command, directory, label, *, prefix="", timeout=3600):
101
+ """Run one FFmpeg command, keeping ``<prefix><label>.command.json`` and ``.log``."""
102
+ directory = Path(directory)
103
+ name = f"{prefix}{label}"
104
+ write_json_atomic(directory / f"{name}.command.json", command)
105
+ result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
106
+ (directory / f"{name}.log").write_text(result.stderr, encoding="utf-8")
107
+ if result.returncode:
108
+ raise RuntimeError(f"{name} FFmpeg failed; see {name}.log")
109
+ return result