@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,469 @@
1
+ #!/usr/bin/env python3
2
+ """Prepare exact source-audio and continuous-score beds from a strict sample plan."""
3
+
4
+ import argparse
5
+ from fractions import Fraction
6
+ import json
7
+ import os
8
+ import math
9
+ from pathlib import Path
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+
14
+ from assemble_constants import SUPPORTED_PICTURE_CODECS, frame_clock_samples
15
+ from adoption.strict_inputs import (
16
+ canonical_fraction, probe_json, read_json_bytes, require_declared_path, require_fields,
17
+ require_integer, require_local_path, require_number, run_logged, without_digests,
18
+ write_json_atomic,
19
+ )
20
+
21
+
22
+ RATE = 48_000
23
+ CHANNELS = 2
24
+ CODEC = "pcm_f32le"
25
+ SOURCE_ROLES = {"protected_original", "mixed_original_under_narration"}
26
+ FADE_SHAPES = {"linear", "half_cosine"}
27
+
28
+
29
+ def _probe_cfr(path):
30
+ data = probe_json(
31
+ path, "-select_streams", "v:0", "-show_streams", "-show_packets",
32
+ "-show_entries",
33
+ "stream=codec_name,time_base,start_pts,start_time,duration_ts,duration,"
34
+ "avg_frame_rate,r_frame_rate:packet=pts,duration",
35
+ )
36
+ streams = data.get("streams", [])
37
+ if len(streams) != 1:
38
+ raise ValueError("source requires exactly one selected v:0 clock")
39
+ stream = streams[0]
40
+ codec = stream.get("codec_name")
41
+ if codec not in SUPPORTED_PICTURE_CODECS:
42
+ raise ValueError("v1 packet/frame CFR proof supports H264 and HEVC picture sources only")
43
+ average = canonical_fraction(stream.get("avg_frame_rate"), "actual average frame rate")
44
+ real = canonical_fraction(stream.get("r_frame_rate"), "actual real frame rate")
45
+ if average != real:
46
+ raise ValueError("source must be same-speed CFR")
47
+ start_time = Fraction(stream.get("start_time", "0"))
48
+ if start_time != 0:
49
+ raise ValueError("source video frame clock must start at zero")
50
+ time_base = Fraction(stream["time_base"])
51
+ packets = data.get("packets", [])
52
+ try:
53
+ pts = sorted(Fraction(int(packet["pts"])) * time_base for packet in packets)
54
+ durations = [Fraction(int(packet["duration"])) * time_base for packet in packets]
55
+ except (KeyError, TypeError, ValueError, ZeroDivisionError) as exc:
56
+ raise ValueError("H26x packet/frame clock proof is incomplete") from exc
57
+ frame_duration = 1 / average
58
+ count = len(pts)
59
+ if not pts or any(value != index * frame_duration for index, value in enumerate(pts)):
60
+ raise ValueError("source picture packets do not prove a zero-origin CFR frame clock")
61
+ if any(duration != frame_duration for duration in durations):
62
+ raise ValueError("source picture packet durations are not one CFR frame")
63
+ return {"fps": str(average), "frame_count": count,
64
+ "time_base": stream.get("time_base"), "start_time": "0",
65
+ "codec_name": codec, "clock_proof": "H26X_ONE_PACKET_PER_FRAME_PTS"}
66
+
67
+
68
+ def _probe_audio(path, ordinal):
69
+ data = probe_json(
70
+ path, "-select_streams", f"a:{ordinal}", "-show_streams",
71
+ "-show_entries", "stream=index,codec_name,sample_fmt,sample_rate,channels,"
72
+ "channel_layout,time_base,start_pts,start_time,duration_ts,duration",
73
+ )
74
+ streams = data.get("streams", [])
75
+ if len(streams) != 1:
76
+ raise ValueError(f"audio stream a:{ordinal} is missing or ambiguous")
77
+ stream = streams[0]
78
+ return {key: stream.get(key) for key in (
79
+ "index", "codec_name", "sample_fmt", "sample_rate", "channels",
80
+ "channel_layout", "time_base", "start_pts", "start_time", "duration_ts", "duration"
81
+ )}
82
+
83
+
84
+ def _probe_pcm(path):
85
+ stream = _probe_audio(path, 0)
86
+ if stream["codec_name"] not in {"pcm_s16le", "pcm_s24le", "pcm_f32le"}:
87
+ raise ValueError("canonical/frozen WAV requires PCM16, PCM24, or PCM float")
88
+ if stream["sample_rate"] != str(RATE) or stream["channels"] != CHANNELS:
89
+ raise ValueError("canonical/frozen WAV must be 48 kHz stereo")
90
+ time_base = Fraction(stream["time_base"])
91
+ samples = int(Fraction(stream["duration_ts"]) * time_base * RATE)
92
+ if Fraction(stream["duration_ts"]) * time_base * RATE != samples:
93
+ raise ValueError("WAV duration is not an integral 48 kHz sample count")
94
+ return {"codec_name": stream["codec_name"], "sample_fmt": stream["sample_fmt"],
95
+ "sample_rate": RATE, "channels": CHANNELS, "samples": samples}
96
+
97
+
98
+ def _validate_fades(value, duration, curves, label):
99
+ fade_in = require_integer(value["fade_in_samples"], f"{label} fade_in_samples")
100
+ fade_out = require_integer(value["fade_out_samples"], f"{label} fade_out_samples")
101
+ if fade_in + fade_out > duration:
102
+ raise ValueError(f"{label} fades overlap")
103
+ if fade_in == 1 or fade_out == 1:
104
+ raise ValueError(f"{label} fades require zero or at least two samples")
105
+ if value["fade_shape"] not in curves:
106
+ raise ValueError(f"unsupported {label} fade_shape")
107
+ return fade_in, fade_out
108
+
109
+
110
+ def load_plan(plan_path):
111
+ plan_path, _, plan = read_json_bytes(plan_path, "source score plan")
112
+ require_fields(plan, ["artifact", "schema_version", "output", "source_segments",
113
+ "source_silence", "score"], "source score plan")
114
+ if plan["artifact"] != "source_score_plan" or type(plan["schema_version"]) is not int \
115
+ or plan["schema_version"] != 1:
116
+ raise ValueError("unsupported source_score_plan schema")
117
+ require_fields(plan["output"], ["sample_rate", "channels", "total_samples"], "output")
118
+ if plan["output"]["sample_rate"] != RATE or plan["output"]["channels"] != CHANNELS:
119
+ raise ValueError("v1 output is fixed at 48 kHz stereo")
120
+ total = require_integer(plan["output"]["total_samples"], "total_samples", 1)
121
+ if not isinstance(plan["source_segments"], list) or not isinstance(plan["source_silence"], list):
122
+ raise ValueError("source segments and explicit silence must be lists")
123
+ segments = []
124
+ asset_cache = {}
125
+ picture_cache = {}
126
+ ids = set()
127
+ segment_fields = [
128
+ "id", "path", "audio_stream", "source_fps", "source_start_frame",
129
+ "source_end_frame", "output_start_sample", "gain", "fade_in_samples",
130
+ "fade_out_samples", "fade_shape", "role",
131
+ ]
132
+ for value in plan["source_segments"]:
133
+ value = without_digests(value, "source segment")
134
+ require_fields(value, segment_fields, "source segment")
135
+ if not isinstance(value["id"], str) or not value["id"] or value["id"] in ids:
136
+ raise ValueError("source segment id must be unique and non-empty")
137
+ ids.add(value["id"])
138
+ ordinal = require_integer(value["audio_stream"], "audio_stream")
139
+ path = require_declared_path(value, "source")
140
+ fps = canonical_fraction(value["source_fps"], "source_fps")
141
+ start = require_integer(value["source_start_frame"], "source_start_frame")
142
+ end = require_integer(value["source_end_frame"], "source_end_frame", 1)
143
+ if end <= start:
144
+ raise ValueError("source frame interval must be non-empty")
145
+ key = (str(path), ordinal)
146
+ if key not in asset_cache:
147
+ if str(path) not in picture_cache:
148
+ picture_cache[str(path)] = _probe_cfr(path)
149
+ picture = picture_cache[str(path)]
150
+ audio = _probe_audio(path, ordinal)
151
+ asset_cache[key] = {"path": str(path), "audio_stream": ordinal,
152
+ "picture": picture, "audio": audio}
153
+ facts = asset_cache[key]
154
+ if facts["picture"]["fps"] != str(fps) or end > facts["picture"]["frame_count"]:
155
+ raise ValueError("declared source frame clock/range differs from actual CFR source")
156
+ source_start_sample = frame_clock_samples(start, fps, RATE)
157
+ source_end_sample = frame_clock_samples(end, fps, RATE)
158
+ duration = source_end_sample - source_start_sample
159
+ if duration <= 0:
160
+ raise ValueError("source frame interval is shorter than one 48 kHz sample")
161
+ fade_in, fade_out = _validate_fades(value, duration, {"linear"}, "source")
162
+ output_start = require_integer(value["output_start_sample"], "output_start_sample")
163
+ output_end = output_start + duration
164
+ if output_end > total or value["role"] not in SOURCE_ROLES:
165
+ raise ValueError("source output range or role is unsupported")
166
+ segments.append({**value, "path": str(path),
167
+ "gain": require_number(value["gain"], "source gain", 0, 16),
168
+ "source_start_sample": source_start_sample,
169
+ "source_end_sample": source_end_sample,
170
+ "output_end_sample": output_end, "fade_in_samples": fade_in,
171
+ "fade_out_samples": fade_out, "asset_key": key})
172
+ silence = []
173
+ for value in plan["source_silence"]:
174
+ require_fields(value, ["output_start_sample", "output_end_sample", "role"], "source silence")
175
+ start = require_integer(value["output_start_sample"], "silence output_start_sample")
176
+ end = require_integer(value["output_end_sample"], "silence output_end_sample", 1)
177
+ if value["role"] != "silence" or not start < end <= total:
178
+ raise ValueError("invalid explicit source silence range")
179
+ silence.append(dict(value))
180
+ coverage = sorted(
181
+ [(item["output_start_sample"], item["output_end_sample"]) for item in segments]
182
+ + [(item["output_start_sample"], item["output_end_sample"]) for item in silence]
183
+ )
184
+ cursor = 0
185
+ for start, end in coverage:
186
+ if start != cursor:
187
+ raise ValueError("source bed requires exact nonoverlapping coverage with explicit silence")
188
+ cursor = end
189
+ if cursor != total:
190
+ raise ValueError("source bed has an implicit tail gap")
191
+ score = plan["score"]
192
+ if not isinstance(score, dict) or score.get("kind") not in {"raw", "frozen", "none"}:
193
+ raise ValueError("score kind must be raw, frozen, or none")
194
+ score = without_digests(score, "score")
195
+ if score["kind"] == "raw":
196
+ require_fields(score, ["kind", "path", "audio_stream", "source_offset_sample",
197
+ "gain", "fade_in_samples", "fade_out_samples", "fade_shape"], "raw score")
198
+ score_path = require_declared_path(score, "raw score")
199
+ ordinal = require_integer(score["audio_stream"], "score audio_stream")
200
+ offset = require_integer(score["source_offset_sample"], "score source_offset_sample")
201
+ fade_in, fade_out = _validate_fades(score, total, FADE_SHAPES, "score")
202
+ score = {**score, "path": str(score_path),
203
+ "audio_stream": ordinal, "source_offset_sample": offset,
204
+ "gain": require_number(score["gain"], "score gain", 0, 16),
205
+ "fade_in_samples": fade_in, "fade_out_samples": fade_out,
206
+ "audio": _probe_audio(score_path, ordinal)}
207
+ elif score["kind"] == "frozen":
208
+ require_fields(score, ["kind", "path", "audio_stream"], "frozen score")
209
+ score_path = require_declared_path(score, "frozen score")
210
+ ordinal = require_integer(score["audio_stream"], "score audio_stream")
211
+ if ordinal != 0:
212
+ raise ValueError("frozen WAV supports only a:0")
213
+ pcm = _probe_pcm(score_path)
214
+ if pcm["samples"] != total:
215
+ raise ValueError("frozen score must exactly match total_samples")
216
+ score = {**score, "path": str(score_path), "pcm": pcm}
217
+ else:
218
+ require_fields(score, ["kind"], "none score")
219
+ return {"plan_path": plan_path,
220
+ "output": {**plan["output"], "codec": CODEC}, "source_segments": segments,
221
+ "source_silence": silence, "source_assets": list(asset_cache.values()),
222
+ "score": score}
223
+
224
+
225
+ _run = run_logged
226
+
227
+
228
+ def _decode_command(path, ordinal, output):
229
+ return ["ffmpeg", "-nostdin", "-v", "error", "-n", "-copyts", "-i", str(path),
230
+ "-map", f"0:a:{ordinal}", "-af",
231
+ "aresample=48000:async=0:first_pts=0,aformat=sample_fmts=flt:"
232
+ "sample_rates=48000:channel_layouts=stereo", "-c:a", CODEC, str(output)]
233
+
234
+
235
+ def _fade_filters(duration, fade_in, fade_out, curve):
236
+ factors = []
237
+ if fade_in:
238
+ if curve == "linear":
239
+ factors.append(f"min(1\\,n/{fade_in - 1})")
240
+ else:
241
+ position = f"max(0\\,min(1\\,n/{fade_in - 1}))"
242
+ factors.append(f"0.5-0.5*cos(PI*{position})")
243
+ if fade_out:
244
+ remaining = f"({duration - 1}-n)"
245
+ if curve == "linear":
246
+ factors.append(f"max(0\\,min(1\\,{remaining}/{fade_out - 1}))")
247
+ else:
248
+ position = f"max(0\\,min(1\\,{remaining}/{fade_out - 1}))"
249
+ factors.append(
250
+ f"0.5-0.5*cos(PI*{position})"
251
+ )
252
+ if not factors:
253
+ return []
254
+ gain = "*".join(f"({factor})" for factor in factors)
255
+ return [f"aeval=val(0)*({gain})|val(1)*({gain}):c=same"]
256
+
257
+
258
+ def _render_source(plan, decoded, output, directory):
259
+ total = plan["output"]["total_samples"]
260
+ command = ["ffmpeg", "-nostdin", "-v", "error", "-n"]
261
+ assets = {tuple(asset_key): index for index, asset_key in enumerate(decoded)}
262
+ for asset_key in decoded:
263
+ command += ["-i", str(decoded[asset_key])]
264
+ filters = [f"anullsrc=r={RATE}:cl=stereo,atrim=end_sample={total}[base]"]
265
+ inputs = ["[base]"]
266
+ for index, segment in enumerate(plan["source_segments"]):
267
+ duration = segment["source_end_sample"] - segment["source_start_sample"]
268
+ chain = [f"[{assets[tuple(segment['asset_key'])]}:a]atrim="
269
+ f"start_sample={segment['source_start_sample']}:"
270
+ f"end_sample={segment['source_end_sample']}", "asetpts=PTS-STARTPTS",
271
+ f"volume={segment['gain']:.17g}"]
272
+ chain += _fade_filters(duration, segment["fade_in_samples"],
273
+ segment["fade_out_samples"], "linear")
274
+ chain.append(f"adelay={segment['output_start_sample']}S:all=1[src{index}]")
275
+ filters.append(",".join(chain))
276
+ inputs.append(f"[src{index}]")
277
+ filters.append("".join(inputs) + f"amix=inputs={len(inputs)}:duration=first:normalize=0,"
278
+ f"atrim=end_sample={total},aformat=sample_fmts=flt:"
279
+ f"sample_rates={RATE}:channel_layouts=stereo[out]")
280
+ command += ["-filter_complex", ";".join(filters), "-map", "[out]", "-c:a", CODEC,
281
+ str(output)]
282
+ _run(command, directory, "source")
283
+
284
+
285
+ def _render_score(plan, decoded_score, output, directory):
286
+ total = plan["output"]["total_samples"]
287
+ score = plan["score"]
288
+ if score["kind"] == "none":
289
+ command = [
290
+ "ffmpeg", "-nostdin", "-v", "error", "-n", "-f", "lavfi", "-i",
291
+ f"anullsrc=r={RATE}:cl=stereo", "-af", f"atrim=end_sample={total}",
292
+ "-c:a", CODEC, str(output),
293
+ ]
294
+ elif score["kind"] == "frozen":
295
+ command = ["ffmpeg", "-nostdin", "-v", "error", "-n", "-i", score["path"],
296
+ "-map", "0:a:0", "-af", "aformat=sample_fmts=flt:sample_rates=48000:"
297
+ "channel_layouts=stereo", "-c:a", CODEC, str(output)]
298
+ else:
299
+ chain = [f"atrim=start_sample={score['source_offset_sample']}:"
300
+ f"end_sample={score['source_offset_sample'] + total}",
301
+ "asetpts=PTS-STARTPTS", f"volume={score['gain']:.17g}"]
302
+ chain += _fade_filters(total, score["fade_in_samples"], score["fade_out_samples"],
303
+ score["fade_shape"])
304
+ chain += [f"atrim=end_sample={total}", "aformat=sample_fmts=flt:sample_rates=48000:"
305
+ "channel_layouts=stereo"]
306
+ command = ["ffmpeg", "-nostdin", "-v", "error", "-n", "-i", str(decoded_score),
307
+ "-af", ",".join(chain), "-c:a", CODEC, str(output)]
308
+ _run(command, directory, "score")
309
+
310
+
311
+ def _astats(path):
312
+ result = subprocess.run(
313
+ ["ffmpeg", "-nostdin", "-v", "info", "-i", str(path), "-af",
314
+ "astats=metadata=0:reset=0", "-f", "null", "-"],
315
+ capture_output=True, text=True, timeout=3600,
316
+ )
317
+ if result.returncode:
318
+ raise ValueError("output astats decode failed")
319
+ peaks = re.findall(r"Peak level dB:\s*(-?inf|[-+0-9.]+)", result.stderr, re.I)
320
+ nan_counts = re.findall(r"Number of NaNs:\s*(\d+)", result.stderr)
321
+ inf_counts = re.findall(r"Number of Infs:\s*(\d+)", result.stderr)
322
+ if not peaks:
323
+ raise ValueError("output peak statistics unavailable")
324
+ peak_db = max(float(value) if value.lower() != "-inf" else -math.inf for value in peaks)
325
+ finite = all(int(value) == 0 for value in nan_counts + inf_counts)
326
+ if not finite:
327
+ raise ValueError("output contains non-finite PCM samples")
328
+ return {"finite": True, "peak": 0.0 if peak_db == -math.inf else 10 ** (peak_db / 20)}
329
+
330
+
331
+ def _output_facts(path):
332
+ """PCM format, sample count, size and peak/finiteness of one 48 kHz stereo float WAV."""
333
+ return {"path": str(path), "bytes": os.stat(path).st_size, "pcm": _probe_pcm(path),
334
+ **_astats(path)}
335
+
336
+
337
+ def validate_prepared_receipt(reference, expected_format):
338
+ """Validate one completed prepared-bed receipt and its three current PCM stems."""
339
+ require_fields(without_digests(reference, "prepared receipt reference"), ["path"],
340
+ "prepared receipt reference")
341
+ require_fields(expected_format, ["sample_rate", "channels", "total_samples"],
342
+ "expected prepared format")
343
+ if expected_format["sample_rate"] != RATE or expected_format["channels"] != CHANNELS:
344
+ raise ValueError("prepared format must be 48 kHz stereo")
345
+ require_integer(expected_format["total_samples"], "prepared total_samples", 1)
346
+ receipt_path = require_declared_path(reference, "prepared receipt")
347
+ receipt = read_json_bytes(receipt_path, "prepared receipt")[2]
348
+ if not isinstance(receipt, dict) or receipt.get("artifact") != "prepared_bed_receipt" \
349
+ or receipt.get("schema_version") != 1 or receipt.get("status") != "PREPARED":
350
+ raise ValueError("prepared receipt is not a completed v1 artifact")
351
+ required_format = {**expected_format, "codec": CODEC}
352
+ if receipt.get("format") != required_format:
353
+ raise ValueError("prepared receipt format differs from expected picture clock")
354
+ outputs = receipt.get("outputs")
355
+ if not isinstance(outputs, dict) or set(outputs) != {
356
+ "source_bed.wav", "score_bed.wav", "prepared_bed.wav"
357
+ }:
358
+ raise ValueError("prepared receipt requires exactly three named bed outputs")
359
+ prepared = {}
360
+ for name, declared in outputs.items():
361
+ if not isinstance(declared, dict):
362
+ raise ValueError(f"prepared receipt {name} identity is invalid")
363
+ path = require_local_path(declared.get("path"), name)
364
+ stream = _probe_audio(path, 0)
365
+ if stream["codec_name"] != CODEC or stream["sample_rate"] != str(RATE) or \
366
+ stream["channels"] != CHANNELS or \
367
+ (stream["start_time"] not in (None, "N/A") and
368
+ Fraction(stream["start_time"]) != 0):
369
+ raise ValueError(f"{name} must be zero-origin 48 kHz stereo float PCM")
370
+ actual = _output_facts(path)
371
+ if actual["pcm"]["codec_name"] != CODEC \
372
+ or actual["pcm"]["samples"] != expected_format["total_samples"]:
373
+ raise ValueError(f"{name} does not match the required full PCM format")
374
+ prepared[name] = actual
375
+ return {
376
+ "reference": {"path": str(receipt_path)},
377
+ "format": dict(expected_format), "outputs": prepared, "receipt": receipt,
378
+ }
379
+
380
+
381
+ def prepare_source_score(plan_path, output_dir):
382
+ directory = Path(output_dir).resolve()
383
+ directory.mkdir(parents=True, exist_ok=False)
384
+ finals = {name: directory / name for name in
385
+ ("source_bed.wav", "score_bed.wav", "prepared_bed.wav")}
386
+ staged = {name: directory / f".{Path(name).stem}.rendering.wav" for name in finals}
387
+ receipt_path = directory / "prepared_bed_receipt.json"
388
+ try:
389
+ plan = load_plan(plan_path)
390
+ decoded = {}
391
+ for index, asset in enumerate(plan["source_assets"]):
392
+ key = (asset["path"], asset["audio_stream"])
393
+ path = directory / f".source_{index:03d}.decoded.wav"
394
+ _run(_decode_command(asset["path"], asset["audio_stream"], path),
395
+ directory, f"decode_source_{index:03d}")
396
+ decoded[key] = path
397
+ asset["canonical_pcm"] = _output_facts(path)
398
+ required = max(
399
+ segment["source_end_sample"] for segment in plan["source_segments"]
400
+ if tuple(segment["asset_key"]) == key
401
+ )
402
+ if asset["canonical_pcm"]["pcm"]["samples"] < required:
403
+ raise ValueError("decoded source audio is too short for a selected frame range")
404
+ score_decode = None
405
+ if plan["score"]["kind"] == "raw":
406
+ score_decode = directory / ".score.decoded.wav"
407
+ _run(_decode_command(plan["score"]["path"], plan["score"]["audio_stream"],
408
+ score_decode), directory, "decode_score")
409
+ plan["score"]["canonical_decode"] = _output_facts(score_decode)
410
+ if plan["score"]["canonical_decode"]["pcm"]["samples"] < \
411
+ plan["score"]["source_offset_sample"] + plan["output"]["total_samples"]:
412
+ raise ValueError("raw score is too short for continuous offset window")
413
+ _render_source(plan, decoded, staged["source_bed.wav"], directory)
414
+ _render_score(plan, score_decode, staged["score_bed.wav"], directory)
415
+ rendered = {
416
+ "source_bed.wav": _output_facts(staged["source_bed.wav"]),
417
+ "score_bed.wav": _output_facts(staged["score_bed.wav"]),
418
+ }
419
+ if plan["score"]["kind"] == "none":
420
+ shutil.copyfile(staged["source_bed.wav"], staged["prepared_bed.wav"])
421
+ else:
422
+ command = [
423
+ "ffmpeg", "-nostdin", "-v", "error", "-n",
424
+ "-i", str(staged["source_bed.wav"]), "-i", str(staged["score_bed.wav"]),
425
+ "-filter_complex", f"[0:a][1:a]amix=inputs=2:duration=first:normalize=0,"
426
+ f"atrim=end_sample={plan['output']['total_samples']},aformat=sample_fmts=flt:"
427
+ "sample_rates=48000:channel_layouts=stereo[out]", "-map", "[out]",
428
+ "-c:a", CODEC, str(staged["prepared_bed.wav"]),
429
+ ]
430
+ _run(command, directory, "prepare")
431
+ outputs = {**rendered, "prepared_bed.wav": _output_facts(staged["prepared_bed.wav"])}
432
+ if any(value["pcm"]["samples"] != plan["output"]["total_samples"]
433
+ for value in outputs.values()):
434
+ raise ValueError("prepared bed sample counts differ from plan")
435
+ if plan["score"]["kind"] == "none" and (
436
+ outputs["prepared_bed.wav"]["bytes"] != outputs["source_bed.wav"]["bytes"]
437
+ or outputs["prepared_bed.wav"]["pcm"] != outputs["source_bed.wav"]["pcm"]):
438
+ raise ValueError("none score must preserve the source bed exactly")
439
+ outputs["prepared_bed.wav"]["headroom_policy"] = "FLOAT_PRESERVED_NO_MASTER"
440
+ receipt = {
441
+ "artifact": "prepared_bed_receipt", "schema_version": 1, "status": "PREPARED",
442
+ "plan": {"path": str(plan["plan_path"])},
443
+ "format": plan["output"], "source_assets": plan["source_assets"],
444
+ "source_segments": [{key: value for key, value in item.items() if key != "asset_key"}
445
+ for item in plan["source_segments"]],
446
+ "source_silence": plan["source_silence"], "score": plan["score"],
447
+ "outputs": outputs, "direct_listening": "NOT_CHECKED", "release_approved": False,
448
+ }
449
+ for name, path in staged.items():
450
+ path.rename(finals[name])
451
+ receipt["outputs"][name]["path"] = str(finals[name])
452
+ write_json_atomic(receipt_path, receipt)
453
+ return receipt
454
+ except Exception:
455
+ for path in [*staged.values(), *finals.values(), receipt_path]:
456
+ path.unlink(missing_ok=True)
457
+ raise
458
+
459
+
460
+ def main():
461
+ parser = argparse.ArgumentParser(description=__doc__)
462
+ parser.add_argument("plan")
463
+ parser.add_argument("--output-dir", required=True)
464
+ args = parser.parse_args()
465
+ print(json.dumps(prepare_source_score(args.plan, args.output_dir), ensure_ascii=False))
466
+
467
+
468
+ if __name__ == "__main__":
469
+ main()
@@ -10,13 +10,15 @@ from lib import CONFIG
10
10
  from media import _plan_clip_spans
11
11
  from assemble_constants import (
12
12
  _AUTO_ORIGINAL_READ_CPS,
13
+ _CLIP_CONTIGUITY_TOLERANCE,
13
14
  _MAX_ORIGINAL_READ_CPS,
14
15
  _MIN_ASR_CLIP_OVERLAP,
15
16
  _MIN_GAP_TO_SUBTITLE,
16
17
  _MIN_READABLE_SECONDS,
17
18
  _SUBTITLE_CLOSING_QUOTES,
18
19
  )
19
- from subtitle_core import (
20
+ from subtitles.track_binding import bound_subtitle_entries
21
+ from subtitles.core import (
20
22
  _bracketed_original_chunks,
21
23
  _subtitle_entries,
22
24
  )
@@ -214,21 +216,32 @@ def _load_user_original_subtitles(work_dir):
214
216
 
215
217
  def _map_asr_to_output(asr_segs, clip_spans):
216
218
  """Map SOURCE-time ASR segments onto the OUTPUT timeline. Full mode (clip_spans None) is
217
- identity; cut mode intersects each ASR span with each kept clip (a straddling line yields one
218
- fragment per clip; lines in cut-away footage are dropped)."""
219
+ identity. Keep one utterance across source/output-continuous picture cuts;
220
+ real source deletions, output gaps and repeated playback remain separate."""
219
221
  if clip_spans is None:
220
222
  return [dict(s) for s in asr_segs]
221
223
  out = []
222
224
  for seg in asr_segs:
225
+ fragments = []
223
226
  for c in clip_spans:
224
227
  ov_s, ov_e = max(seg["start"], c["source_start"]), min(seg["end"], c["source_end"])
225
- if ov_e - ov_s <= _MIN_ASR_CLIP_OVERLAP:
228
+ if ov_e <= ov_s:
226
229
  continue
227
- out.append({
228
- "start": c["output_start"] + (ov_s - c["source_start"]),
229
- "end": c["output_start"] + (ov_e - c["source_start"]),
230
- "text": seg["text"],
231
- })
230
+ start = c["output_start"] + (ov_s - c["source_start"])
231
+ end = c["output_start"] + (ov_e - c["source_start"])
232
+ entry = c.get("entry", {})
233
+ source_key = (c.get("source_id") or entry.get("source_id"),
234
+ c.get("source_path") or entry.get("source_path"))
235
+ if (fragments and abs(fragments[-1]["source_end"] - ov_s) < _CLIP_CONTIGUITY_TOLERANCE
236
+ and abs(fragments[-1]["end"] - start) < _CLIP_CONTIGUITY_TOLERANCE
237
+ and fragments[-1]["source_key"] == source_key):
238
+ fragments[-1].update(end=end, source_end=ov_e)
239
+ else:
240
+ fragments.append({"start": start, "end": end, "source_end": ov_e,
241
+ "source_key": source_key})
242
+ # Filter after joining: individually tiny pieces may be one readable phrase.
243
+ out.extend({"start": f["start"], "end": f["end"], "text": seg["text"]}
244
+ for f in fragments if f["end"] - f["start"] > _MIN_ASR_CLIP_OVERLAP)
232
245
  return out
233
246
 
234
247
 
@@ -394,6 +407,9 @@ def _sentence_subspan(seg, sentence):
394
407
  def _combined_subtitle_entries(narration, work_dir, video_duration):
395
408
  """Narration subtitle entries plus original-dialogue entries in the gaps, sorted by start.
396
409
  Original entries are confined to narration gaps, so they never overlap narration entries."""
410
+ bound = bound_subtitle_entries(work_dir, video_duration)
411
+ if bound is not None:
412
+ return bound
397
413
  entries = _subtitle_entries(narration)
398
414
  entries.extend(_original_gap_subtitle_entries(narration, work_dir, video_duration))
399
415
  entries.sort(key=lambda x: (x["start"], x["end"]))
@@ -0,0 +1 @@
1
+ """Subtitle generation and track-binding subpackage."""
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
  import re
5
+ from decimal import Decimal
5
6
 
6
7
  from lib import CONFIG
7
8
  from assemble_constants import (
@@ -13,11 +14,16 @@ from assemble_constants import (
13
14
  from media import _ratio_to_float
14
15
 
15
16
  def _seconds_to_srt_time(seconds):
16
- """将秒数转为 SRT 时间格式 HH:MM:SS,mmm"""
17
- h = int(seconds // 3600)
18
- m = int((seconds % 3600) // 60)
19
- s = int(seconds % 60)
20
- ms = int((seconds % 1) * 1000)
17
+ """Floor times to SRT milliseconds without float remainder artifacts.
18
+
19
+ Coercing first keeps Fraction/Decimal/str inputs working and clamps a negative
20
+ time to zero instead of emitting a negative-component SRT stamp.
21
+ """
22
+ seconds = max(0.0, float(seconds))
23
+ total_ms = int(Decimal(str(seconds)) * 1000)
24
+ h, remainder = divmod(total_ms, 3_600_000)
25
+ m, remainder = divmod(remainder, 60_000)
26
+ s, ms = divmod(remainder, 1000)
21
27
  return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
22
28
 
23
29
 
@@ -45,6 +51,7 @@ def _subtitle_style_config(canvas=None):
45
51
  """
46
52
  style = {
47
53
  "font_name": CONFIG["subtitle_font_name"],
54
+ "font_file": CONFIG["subtitle_font_file"],
48
55
  "font_size": CONFIG["subtitle_font_size"],
49
56
  "primary_color": CONFIG["subtitle_primary_color"],
50
57
  "outline_color": CONFIG["subtitle_outline_color"],
@@ -278,7 +285,7 @@ def _subtitle_entries(narration):
278
285
  max_chars = CONFIG["subtitle_max_chars"]
279
286
  entries = []
280
287
  for seg in narration:
281
- text = seg.get("spoken_text", seg["narration"])
288
+ text = seg["spoken_text"]
282
289
  start, end = float(seg["actual_place_start"]), float(seg["actual_place_end"])
283
290
  entries.extend(_distribute_chunks(_split_subtitle_chunks(text, max_chars), start, end))
284
291
  return entries
@@ -1,7 +1,7 @@
1
1
  """SRT/ASS serialization for narration and original-dialogue subtitles."""
2
2
 
3
3
  from source_subtitles import _combined_subtitle_entries
4
- from subtitle_core import (
4
+ from subtitles.core import (
5
5
  _normalize_subtitle_text,
6
6
  _seconds_to_ass_time,
7
7
  _seconds_to_srt_time,
@@ -16,7 +16,7 @@ def _generate_srt(narration, work_dir, video_duration):
16
16
  for idx, entry in enumerate(_combined_subtitle_entries(narration, work_dir, video_duration), start=1):
17
17
  srt_lines.append(str(idx))
18
18
  srt_lines.append(f"{_seconds_to_srt_time(entry['start'])} --> {_seconds_to_srt_time(entry['end'])}")
19
- srt_lines.append(_normalize_subtitle_text(entry["text"]))
19
+ srt_lines.append(entry["text"] if entry.get("_bound_track") else _normalize_subtitle_text(entry["text"]))
20
20
  srt_lines.append("")
21
21
  srt_path = work_dir / "subtitles.srt"
22
22
  srt_path.write_text("\n".join(srt_lines), encoding="utf-8")
@@ -69,10 +69,11 @@ def _generate_ass(narration, work_dir, video_duration, canvas):
69
69
  ]
70
70
  # entries are already split into short one-line chunks, so no wrapping here.
71
71
  for entry in _combined_subtitle_entries(narration, work_dir, video_duration):
72
- text = _escape_ass_text(_normalize_subtitle_text(entry["text"]))
72
+ text = _escape_ass_text(entry["text"] if entry.get("_bound_track") else _normalize_subtitle_text(entry["text"]))
73
73
  ass_lines.append(
74
74
  "Dialogue: 0,"
75
- f"{_seconds_to_ass_time(entry['start'])},{_seconds_to_ass_time(entry['end'])},"
75
+ f"{entry.get('ass_start', _seconds_to_ass_time(entry['start']))},"
76
+ f"{entry.get('ass_end', _seconds_to_ass_time(entry['end']))},"
76
77
  f"Default,,0,0,0,,{text}"
77
78
  )
78
79