@oh-story/dsh 0.1.8 → 0.1.9

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 (272) hide show
  1. package/README.md +11 -11
  2. package/lib/client.js +35 -14
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +165 -69
  5. package/lib/drama/skills/short-drama/SKILL.md +5 -1
  6. package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
  7. package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
  8. package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
  9. package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
  10. package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
  11. package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
  12. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
  13. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
  14. package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
  15. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
  16. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
  17. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
  18. package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
  19. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
  20. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
  21. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
  22. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
  23. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
  24. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
  25. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
  26. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
  27. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
  28. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
  29. package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
  30. package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
  31. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
  32. package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
  33. package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
  34. package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
  35. package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
  36. package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
  37. package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
  38. package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
  39. package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
  40. package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
  41. package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
  42. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
  43. package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
  44. package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
  45. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
  46. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
  47. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
  48. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
  49. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
  50. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
  51. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
  52. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
  53. package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
  54. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
  55. package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
  56. package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
  57. package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
  58. package/lib/index.js +4 -2
  59. package/lib/index.js.map +2 -2
  60. package/lib/novel-to-game/VERSION +1 -1
  61. package/lib/novel-to-game/manifest.json +28 -28
  62. package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
  63. package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
  64. package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
  65. package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
  66. package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
  67. package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
  68. package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
  69. package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
  70. package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
  71. package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
  72. package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
  73. package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
  74. package/lib/oh-story/manifest.json +292 -172
  75. package/lib/oh-story/roles/character-designer.md +1 -1
  76. package/lib/oh-story/roles/narrative-writer.md +56 -242
  77. package/lib/oh-story/roles/story-architect.md +1 -1
  78. package/lib/oh-story/skills/story/SKILL.md +1 -1
  79. package/lib/oh-story/skills/story/VERSION +1 -1
  80. package/lib/oh-story/skills/story/references/author-memory.md +10 -8
  81. package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
  82. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
  83. package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
  84. package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
  85. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
  86. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
  87. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
  88. package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
  89. package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
  90. package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
  91. package/lib/oh-story/skills/story-import/SKILL.md +2 -2
  92. package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
  93. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
  94. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
  95. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
  96. package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
  97. package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
  98. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
  99. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
  100. package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
  101. package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
  102. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
  103. package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
  104. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
  105. package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
  106. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
  107. package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
  108. package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
  109. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
  110. package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
  111. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  112. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
  113. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
  114. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
  115. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
  116. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
  117. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
  118. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
  119. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
  120. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
  121. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
  122. package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
  123. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
  124. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
  125. package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
  126. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
  127. package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
  128. package/lib/oh-story/skills/story-review/SKILL.md +13 -3
  129. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
  130. package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
  131. package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
  132. package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
  133. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
  134. package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
  135. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
  136. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
  137. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
  138. package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
  139. package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
  140. package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
  141. package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
  142. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
  143. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
  144. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
  145. package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
  146. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
  147. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
  148. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
  149. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  150. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  151. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
  152. package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
  153. package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
  154. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
  155. package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
  156. package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
  157. package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
  158. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
  159. package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
  160. package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
  161. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
  162. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
  163. package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
  164. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
  165. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
  166. package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
  167. package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
  168. package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
  169. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
  170. package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
  171. package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
  172. package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
  173. package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
  174. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
  175. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
  176. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
  177. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
  178. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
  179. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
  180. package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
  181. package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
  182. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
  183. package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
  184. package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
  185. package/lib/types/client/drama-production-view.d.ts.map +1 -1
  186. package/lib/types/client/index.d.ts.map +1 -1
  187. package/lib/types/client/production-prompts.d.ts.map +1 -1
  188. package/lib/types/client/production-runtime.d.ts +17 -0
  189. package/lib/types/client/production-runtime.d.ts.map +1 -1
  190. package/lib/types/skill-provider.d.ts.map +1 -1
  191. package/lib/video-recap/CHANGELOG.md +31 -1
  192. package/lib/video-recap/manifest.json +162 -162
  193. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
  194. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
  195. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
  196. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
  197. package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
  198. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
  199. package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
  200. package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
  201. package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
  202. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
  203. package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
  204. package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
  205. package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
  206. package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
  207. package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
  208. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
  209. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
  210. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
  211. package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
  212. package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
  213. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
  214. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
  215. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
  216. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
  217. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
  218. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
  219. package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
  220. package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
  221. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
  222. package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
  223. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
  224. package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
  225. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
  226. package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
  227. package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
  228. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
  229. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
  230. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
  231. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
  232. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
  233. package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
  234. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
  235. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
  236. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
  237. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
  238. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
  239. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
  240. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
  241. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
  242. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
  243. package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
  244. package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
  245. package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
  246. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
  247. package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
  248. package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
  249. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
  250. package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
  251. package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
  252. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
  253. package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
  254. package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
  255. package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
  256. package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
  257. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
  258. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
  259. package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
  260. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
  261. package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
  262. package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
  263. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
  264. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
  265. package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
  266. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
  267. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
  268. package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
  269. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
  270. package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
  271. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
  272. package/package.json +41 -41
@@ -1,26 +1,23 @@
1
1
  """Loudness, source handoffs, ducking envelopes, and audio mix graphs."""
2
2
 
3
3
  import json
4
- import math
5
4
  import re
6
5
  from pathlib import Path
7
6
 
8
7
  from artifacts import _load_work_json, _value_fingerprint
9
8
  from audio_automation import (
10
9
  coalesce_duck_windows,
11
- default_bridge,
12
10
  ducking_expression,
13
11
  release_ducking_expression,
14
12
  )
15
13
  from lib import CONFIG, log, run_cmd
16
14
 
17
15
  def _limiter_filter():
18
- peak = float(CONFIG.get("final_limiter_peak", 0.98) or 0.98)
19
- return f"alimiter=limit={peak:.2f}:level=false"
16
+ return f"alimiter=limit={CONFIG['final_limiter_peak']:.2f}:level=false"
20
17
 
21
18
 
22
19
  def _loudness_mode(measured=None):
23
- if not CONFIG.get("final_loudnorm", True):
20
+ if not CONFIG["final_loudnorm"]:
24
21
  return "limiter_only"
25
22
  return "two_pass_linear" if measured else "equivalent"
26
23
 
@@ -34,12 +31,12 @@ def final_loudnorm_filter(measured=None):
34
31
  pass; without it we still force the same target and peak limiter as a
35
32
  documented equivalent/fallback path.
36
33
  """
37
- if not CONFIG.get("final_loudnorm", True):
34
+ if not CONFIG["final_loudnorm"]:
38
35
  return _limiter_filter()
39
36
  filt = (
40
- f"loudnorm=I={CONFIG.get('target_lufs', -14.0)}"
41
- f":TP={CONFIG.get('target_true_peak', -1.0)}"
42
- f":LRA={CONFIG.get('target_lra', 11.0)}"
37
+ f"loudnorm=I={CONFIG['target_lufs']}"
38
+ f":TP={CONFIG['target_true_peak']}"
39
+ f":LRA={CONFIG['target_lra']}"
43
40
  f":linear=true"
44
41
  )
45
42
  if measured:
@@ -58,21 +55,21 @@ def final_loudnorm_filter(measured=None):
58
55
 
59
56
  def _parse_loudnorm_json(text):
60
57
  """Extract ffmpeg loudnorm JSON from stderr/stdout."""
61
- for match in reversed(list(re.finditer(r"\{[\s\S]*?\}", str(text or "")))):
58
+ for match in reversed(list(re.finditer(r"\{[\s\S]*?\}", text))):
62
59
  try:
63
60
  data = json.loads(match.group(0))
64
61
  except ValueError:
65
62
  continue
66
- if isinstance(data, dict) and {"input_i", "input_tp", "input_lra", "input_thresh", "target_offset"} <= set(data):
63
+ if {"input_i", "input_tp", "input_lra", "input_thresh", "target_offset"} <= set(data):
67
64
  return data
68
65
  return None
69
66
 
70
67
 
71
68
  def _loudnorm_first_pass_filter():
72
69
  return (
73
- f"loudnorm=I={CONFIG.get('target_lufs', -14.0)}"
74
- f":TP={CONFIG.get('target_true_peak', -1.0)}"
75
- f":LRA={CONFIG.get('target_lra', 11.0)}"
70
+ f"loudnorm=I={CONFIG['target_lufs']}"
71
+ f":TP={CONFIG['target_true_peak']}"
72
+ f":LRA={CONFIG['target_lra']}"
76
73
  f":print_format=json"
77
74
  )
78
75
 
@@ -84,7 +81,7 @@ def _run_loudnorm_first_pass(input_video, narration_wav, original_audio_input,
84
81
  Returns ffmpeg loudnorm JSON, or None when probing fails. The caller then
85
82
  falls back to the documented equivalent single-pass target+limiter filter.
86
83
  """
87
- if not CONFIG.get("final_loudnorm", True):
84
+ if not CONFIG["final_loudnorm"]:
88
85
  return None
89
86
  probe_fc = f"{filter_complex};[aout]{_loudnorm_first_pass_filter()}[lnprobe]"
90
87
  probe_script = Path(work_dir) / ".filter_complex_loudnorm_probe.txt"
@@ -106,7 +103,7 @@ def _run_loudnorm_first_pass(input_video, narration_wav, original_audio_input,
106
103
  if result.returncode != 0:
107
104
  log(f" ⚠️ loudnorm 首遍测量失败,降级到目标滤镜+limiter: {result.stderr}")
108
105
  return None
109
- measured = _parse_loudnorm_json((result.stdout or "") + "\n" + (result.stderr or ""))
106
+ measured = _parse_loudnorm_json(result.stdout + "\n" + result.stderr)
110
107
  if not measured:
111
108
  log(" ⚠️ loudnorm 首遍未返回 JSON,降级到目标滤镜+limiter")
112
109
  return None
@@ -114,10 +111,8 @@ def _run_loudnorm_first_pass(input_video, narration_wav, original_audio_input,
114
111
 
115
112
 
116
113
  def _seg_place_window(seg):
117
- """Return a segment's actual placed (start, end) on the output timeline."""
118
- s = seg.get("actual_place_start", seg.get("start", 0))
119
- e = seg.get("actual_place_end", seg.get("end", 0))
120
- return s, e
114
+ """A segment's actual placed (start, end) on the output timeline; zero-width when unplaced."""
115
+ return seg["actual_place_start"], seg["actual_place_end"]
121
116
 
122
117
 
123
118
  def _load_sentence_handoff_anchors(work_dir):
@@ -126,94 +121,58 @@ def _load_sentence_handoff_anchors(work_dir):
126
121
  cut_mode = (work_dir / "edited_source.mp4").exists() or (
127
122
  work_dir / "clip_plan_validated.json"
128
123
  ).exists()
129
- candidates = [
130
- work_dir
131
- / (
132
- "speech_boundary_anchors_output.json"
133
- if cut_mode
134
- else "speech_boundary_anchors.json"
124
+ artifact = "speech_boundary_anchors_output.json" if cut_mode else "speech_boundary_anchors.json"
125
+ payload = _load_work_json(work_dir, artifact)
126
+ if payload is None:
127
+ return [], None, {"require_measured": cut_mode}
128
+ if cut_mode:
129
+ # Output-clock anchors are only trusted when they were derived from the current cut plan.
130
+ plan = _load_work_json(work_dir, "clip_plan_validated.json")
131
+ fresh = (
132
+ payload.get("schema_version") == 2
133
+ and payload.get("timeline") == "cut_output"
134
+ and plan is not None
135
+ and payload.get("clip_plan_fingerprint") == _value_fingerprint(plan)
135
136
  )
136
- ]
137
- for path in candidates:
138
- if not path.exists():
139
- continue
140
- try:
141
- payload = json.loads(path.read_text(encoding="utf-8"))
142
- except (OSError, json.JSONDecodeError):
143
- continue
144
- if cut_mode:
145
- plan = _load_work_json(work_dir, "clip_plan_validated.json")
146
- if not (
147
- isinstance(payload, dict)
148
- and payload.get("schema_version") == 2
149
- and payload.get("timeline") == "cut_output"
150
- and isinstance(plan, dict)
151
- and payload.get("clip_plan_fingerprint") == _value_fingerprint(plan)
152
- ):
153
- return [], None, {"require_measured": True}
154
- payload = {**payload, "require_measured": True}
155
- raw = payload.get("sentence_anchors", []) if isinstance(payload, dict) else []
156
- anchors = []
157
- for item in raw:
158
- if not isinstance(item, dict) or item.get("confidence") not in {"high", "medium"}:
159
- continue
160
- try:
161
- when = float(item.get("time"))
162
- except (TypeError, ValueError):
163
- continue
164
- if math.isfinite(when) and when >= 0:
165
- try:
166
- pause_start = float(item.get("pause_start", when - 0.12))
167
- except (TypeError, ValueError):
168
- pause_start = when - 0.12
169
- anchors.append({
170
- "time": round(when, 4),
171
- "pause_start": round(max(0.0, min(pause_start, when)), 4),
172
- })
173
- unique = {(row["time"], row["pause_start"]): row for row in anchors}
174
- return sorted(unique.values(), key=lambda row: row["time"]), path.name, payload
175
- return [], None, {"require_measured": cut_mode}
176
-
177
-
178
- def _handoff_timed_rows(payload, key):
179
- rows = payload.get(key, []) if isinstance(payload, dict) else []
180
- out = []
181
- for row in rows if isinstance(rows, list) else []:
182
- if not isinstance(row, dict):
137
+ if not fresh:
138
+ return [], None, {"require_measured": True}
139
+ payload = {**payload, "require_measured": True}
140
+ anchors = {}
141
+ for item in payload["sentence_anchors"]:
142
+ if item["confidence"] not in {"high", "medium"}:
183
143
  continue
184
- try:
185
- start, end = float(row.get("start")), float(row.get("end"))
186
- except (TypeError, ValueError):
187
- continue
188
- if math.isfinite(start) and math.isfinite(end) and end > start:
189
- out.append({"start": start, "end": end})
190
- return out
144
+ when = float(item["time"])
145
+ pause_start = float(item.get("pause_start", when - 0.12))
146
+ row = {
147
+ "time": round(when, 4),
148
+ "pause_start": round(max(0.0, min(pause_start, when)), 4),
149
+ }
150
+ anchors[(row["time"], row["pause_start"])] = row
151
+ return sorted(anchors.values(), key=lambda row: row["time"]), artifact, payload
152
+
153
+
154
+ def _timed_rows(rows):
155
+ return [{"start": float(row["start"]), "end": float(row["end"])} for row in rows]
191
156
 
192
157
 
193
- def _handoff_speech_evidence(work_dir, artifact, payload):
194
- speech = _handoff_timed_rows(payload, "speech_spans")
195
- quiet = _handoff_timed_rows(payload, "quiet_windows")
158
+ def _asr_segments(work_dir):
159
+ """Cleaned ASR segments (asr_clean.json) when present, else raw asr_result.json; [] when absent."""
160
+ clean = _load_work_json(work_dir, "asr_clean.json")
161
+ if clean is not None:
162
+ return clean["segments"]
163
+ return _load_work_json(work_dir, "asr_result.json") or []
164
+
165
+
166
+ def _handoff_speech_evidence(work_dir, payload):
167
+ speech = _timed_rows(payload.get("speech_spans", []))
168
+ quiet = _timed_rows(payload.get("quiet_windows", []))
196
169
  if payload.get("require_measured"):
197
170
  return speech, quiet
198
- if artifact == "speech_boundary_anchors_output.json":
199
- return speech, quiet
200
171
  if not speech:
201
- for name in ("asr_clean.json", "asr_result.json"):
202
- raw = _load_work_json(work_dir, name)
203
- if isinstance(raw, list):
204
- raw = {"speech_spans": raw}
205
- elif isinstance(raw, dict):
206
- raw = {"speech_spans": raw.get("segments", [])}
207
- speech = _handoff_timed_rows(raw, "speech_spans")
208
- if speech:
209
- break
172
+ speech = _timed_rows(_asr_segments(work_dir))
210
173
  if not quiet:
211
- raw = _load_work_json(work_dir, "silence_periods.json")
212
- raw = {"quiet_windows": [
213
- row for row in raw
214
- if isinstance(row, dict) and not bool(row.get("has_speech", False))
215
- ]} if isinstance(raw, list) else {}
216
- quiet = _handoff_timed_rows(raw, "quiet_windows")
174
+ silence = _load_work_json(work_dir, "silence_periods.json") or []
175
+ quiet = _timed_rows(row for row in silence if not row.get("has_speech", False))
217
176
  return speech, quiet
218
177
 
219
178
 
@@ -250,10 +209,7 @@ def _measured_speech_owned(
250
209
  start, end, speech, quiet, anchors, authored, require_measured=False
251
210
  ):
252
211
  duration = max(0.0, end - start)
253
- quiet_min = max(
254
- 0.3,
255
- duration * float(CONFIG.get("quiet_overlap_min_ratio", 0.8) or 0.8),
256
- )
212
+ quiet_min = max(0.3, duration * CONFIG["quiet_overlap_min_ratio"])
257
213
  if speech:
258
214
  return _speech_overlap_excluding_quiet(start, end, speech, quiet) > 0.05
259
215
  quiet_overlap = sum(
@@ -276,19 +232,10 @@ def _entry_speech_owned(
276
232
  return True if anchors or require_measured else bool(authored)
277
233
 
278
234
 
279
- def _work_has_source_speech(work_dir, speech_spans=None, require_measured=False):
235
+ def _work_has_source_speech(work_dir, speech_spans, require_measured):
280
236
  if speech_spans or require_measured:
281
237
  return True
282
- for name in ("asr_clean.json", "asr_result.json"):
283
- payload = _load_work_json(work_dir, name)
284
- if isinstance(payload, dict):
285
- payload = payload.get("segments", [])
286
- if isinstance(payload, list) and any(
287
- isinstance(item, dict) and str(item.get("text") or "").strip()
288
- for item in payload
289
- ):
290
- return True
291
- return False
238
+ return any(item["text"].strip() for item in _asr_segments(work_dir))
292
239
 
293
240
 
294
241
  def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
@@ -297,21 +244,14 @@ def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
297
244
  This does not move or trim narration. It only extends the ORIGINAL-audio duck
298
245
  envelope so returning the source track cannot reveal the middle of a sentence.
299
246
  """
300
- fade = max(0.0, float(CONFIG.get("duck_fade_seconds", 0.3) or 0.0))
301
- bridge = max(0.0, float(CONFIG.get("duck_bridge_seconds", 1.5) or 0.0))
247
+ fade = CONFIG["duck_fade_seconds"]
248
+ bridge = CONFIG["duck_bridge_seconds"]
302
249
  anchors, artifact, evidence_payload = _load_sentence_handoff_anchors(work_dir)
303
- speech_spans, quiet_windows = _handoff_speech_evidence(
304
- work_dir, artifact, evidence_payload
305
- )
306
- require_measured = bool(evidence_payload.get("require_measured"))
250
+ speech_spans, quiet_windows = _handoff_speech_evidence(work_dir, evidence_payload)
251
+ require_measured = evidence_payload.get("require_measured", False)
307
252
  placed = []
308
- for seg in tts_segments or []:
309
- if not isinstance(seg, dict):
310
- continue
311
- try:
312
- start, end = map(float, _seg_place_window(seg))
313
- except (TypeError, ValueError):
314
- continue
253
+ for seg in tts_segments:
254
+ start, end = _seg_place_window(seg)
315
255
  if end > start:
316
256
  placed.append((start, end, seg))
317
257
  placed.sort(key=lambda item: (item[0], item[1]))
@@ -326,14 +266,12 @@ def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
326
266
  else:
327
267
  runs.append({"start": start, "end": end, "segments": [seg]})
328
268
 
329
- source_has_speech = _work_has_source_speech(
330
- work_dir, speech_spans, require_measured=require_measured
331
- )
269
+ source_has_speech = _work_has_source_speech(work_dir, speech_spans, require_measured)
332
270
  report = []
333
271
  for run in runs:
334
272
  ownership = []
335
273
  for seg in run["segments"]:
336
- start, end = map(float, _seg_place_window(seg))
274
+ start, end = _seg_place_window(seg)
337
275
  measured = _measured_speech_owned(
338
276
  start,
339
277
  end,
@@ -351,7 +289,7 @@ def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
351
289
  speech_spans,
352
290
  quiet_windows,
353
291
  anchors,
354
- first.get("overlaps_speech", True),
292
+ first["overlaps_speech"],
355
293
  require_measured=require_measured,
356
294
  )
357
295
  speech_owned = entry_owned or any(ownership)
@@ -420,60 +358,35 @@ def _amix_tail(narr_vol, bgm_chain=""):
420
358
  return narr + "[orig][narr]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]"
421
359
 
422
360
 
423
- def _placement_windows(tts_segments, level_for, *, source_safe_end=False):
424
- """Collect [(start, end, level)] placement windows for the placed beats, using
425
- `level_for(seg)` to pick each beat's duck level. Skips non-dicts and empty spans."""
426
- windows = []
427
- for seg in tts_segments:
428
- if not isinstance(seg, dict):
429
- continue
430
- s, e = _seg_place_window(seg)
431
- if source_safe_end:
432
- try:
433
- e = max(float(e), float(seg.get("source_duck_end", e)))
434
- except (TypeError, ValueError):
435
- pass
436
- if e - s <= 0:
437
- continue
438
- windows.append((s, e, level_for(seg)))
439
- return windows
440
-
441
-
442
- def _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge=None):
361
+ def _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge):
443
362
  """Per-beat ducking automation for the ORIGINAL track.
444
363
 
445
364
  Uses the shared ducking contract: [start-fade,start] pre-roll ramp down,
446
365
  [start,end] held at the selected duck level, and [end,end+fade] release.
447
366
  Bridged spans use the most-ducked (lowest) level, matching timeline.json /
448
- JianYing keyframes. Returns a volume= expression, or None when no beat carries
449
- placement info (caller falls back to a constant).
367
+ JianYing keyframes. Returns a volume= expression, or None when no beat was
368
+ placed (caller falls back to a constant).
450
369
  """
451
- if bridge is None:
452
- bridge = default_bridge(fade)
453
370
  windows = []
454
- for seg in tts_segments or []:
455
- if not isinstance(seg, dict):
456
- continue
457
- try:
458
- start, narration_end = map(float, _seg_place_window(seg))
459
- hold_end = max(narration_end, float(seg.get("source_duck_end", narration_end)))
460
- restore_at = max(hold_end, float(seg.get("source_restore_at", hold_end + fade)))
461
- except (TypeError, ValueError):
462
- continue
371
+ for seg in tts_segments:
372
+ start, narration_end = _seg_place_window(seg)
463
373
  if narration_end <= start:
464
374
  continue
375
+ hold_end = max(narration_end, seg.get("source_duck_end", narration_end))
376
+ restore_at = max(hold_end, seg.get("source_restore_at", hold_end + fade))
465
377
  level = speech_vol if seg.get("overlaps_speech", True) else quiet_vol
466
378
  windows.append((start, hold_end, level, restore_at))
467
379
  return release_ducking_expression(windows, idle, fade, bridge=bridge)
468
380
 
469
381
 
470
- def _bgm_envelope(tts_segments, base, duck, fade, bridge=None):
382
+ def _bgm_envelope(tts_segments, base, duck, fade, bridge):
471
383
  """Per-beat ducking automation for the BGM track using the shared contract."""
472
- if bridge is None:
473
- bridge = default_bridge(fade)
474
- windows = _placement_windows(tts_segments, lambda _: duck)
475
- merged = coalesce_duck_windows(windows, bridge)
476
- return ducking_expression(merged, base, fade)
384
+ windows = [
385
+ (start, end, duck)
386
+ for start, end in map(_seg_place_window, tts_segments)
387
+ if end > start
388
+ ]
389
+ return ducking_expression(coalesce_duck_windows(windows, bridge), base, fade)
477
390
 
478
391
 
479
392
  def _build_audio_filter_complex(
@@ -481,7 +394,7 @@ def _build_audio_filter_complex(
481
394
  has_bgm=False,
482
395
  *,
483
396
  original_audio_label="0:a",
484
- bgm_audio_label=None,
397
+ bgm_audio_label="2:a",
485
398
  ):
486
399
  """Compose the audio tracks into [aout], like a cut-software timeline.
487
400
 
@@ -495,26 +408,24 @@ def _build_audio_filter_complex(
495
408
  fixed = the gap-fill envelope above; sidechaincompress = auto-duck keyed off the
496
409
  narration; none = no ducking. Placement comes from actual_place_start/end.
497
410
  """
498
- ducking_mode = CONFIG.get("ducking_mode", "fixed")
411
+ ducking_mode = CONFIG["ducking_mode"]
499
412
  if ducking_mode == "sidechaincompress" and any(
500
- isinstance(seg, dict)
501
- and float(seg.get("source_duck_end", seg.get("actual_place_end", 0)) or 0)
502
- > float(seg.get("actual_place_end", 0) or 0) + 1e-6
503
- for seg in tts_segments or []
413
+ "source_duck_end" in seg and seg["source_duck_end"] > seg["actual_place_end"] + 1e-6
414
+ for seg in tts_segments
504
415
  ):
505
416
  log("sidechaincompress 无法保持句末交接窗口,已回退 fixed ducking")
506
417
  ducking_mode = "fixed"
507
- narr_vol = CONFIG.get("ducking_narr_weight", 1.5)
508
- fade = CONFIG.get("duck_fade_seconds", 0.3)
418
+ narr_vol = CONFIG["ducking_narr_weight"]
419
+ fade = CONFIG["duck_fade_seconds"]
420
+ bridge = CONFIG["duck_bridge_seconds"]
509
421
  original_in = f"[{original_audio_label}]"
510
- bgm_in = f"[{bgm_audio_label or '2:a'}]"
422
+ bgm_in = f"[{bgm_audio_label}]"
511
423
 
512
424
  # BGM bed (input [2:a]): ducked under each narration window when present.
513
425
  bgm_chain = ""
514
426
  if has_bgm:
515
- base = CONFIG.get("bgm_volume", 0.18)
516
- bgm_expr = _bgm_envelope(tts_segments, base, CONFIG.get("bgm_ducking_volume", 0.10), fade,
517
- bridge=CONFIG.get("duck_bridge_seconds", 1.5))
427
+ base = CONFIG["bgm_volume"]
428
+ bgm_expr = _bgm_envelope(tts_segments, base, CONFIG["bgm_ducking_volume"], fade, bridge)
518
429
  if bgm_expr:
519
430
  bgm_chain = f"{bgm_in}volume='{bgm_expr}':eval=frame,aresample=48000[bgm];"
520
431
  else:
@@ -539,17 +450,16 @@ def _build_audio_filter_complex(
539
450
  return f"{original_in}aresample=48000[orig];" + _amix_tail(narr_vol, bgm_chain)
540
451
 
541
452
  # fixed (default): gap-fill ducking envelope on the original track.
542
- idle = CONFIG.get("idle_orig_volume", 1.0)
543
- speech_vol = CONFIG.get("speech_ducking_volume", 0.2)
544
- quiet_vol = CONFIG.get("zone_ducking_volume", 0.12)
545
- bridge = CONFIG.get("duck_bridge_seconds", 1.5)
546
- expr = _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge=bridge)
453
+ idle = CONFIG["idle_orig_volume"]
454
+ speech_vol = CONFIG["speech_ducking_volume"]
455
+ quiet_vol = CONFIG["zone_ducking_volume"]
456
+ expr = _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge)
547
457
  if expr:
548
- n_overlap = sum(1 for s in tts_segments if isinstance(s, dict) and s.get("overlaps_speech", True))
549
- n_quiet = sum(1 for s in tts_segments if isinstance(s, dict) and not s.get("overlaps_speech", True))
458
+ n_overlap = sum(1 for s in tts_segments if s.get("overlaps_speech", True))
459
+ n_quiet = len(tts_segments) - n_overlap
550
460
  log(f"gap-fill ducking: 间隙原声={idle}, 对白段={speech_vol}({n_overlap}), 安静段={quiet_vol}({n_quiet}), 桥接间隙<{bridge}s")
551
461
  orig = f"{original_in}volume='{expr}':eval=frame,aresample=48000[orig];"
552
462
  else:
553
463
  # No placement info at all: hold the original at a constant level.
554
- orig = f"{original_in}volume={CONFIG.get('ducking_orig_volume', 0.3)},aresample=48000[orig];"
464
+ orig = f"{original_in}volume={CONFIG['ducking_orig_volume']},aresample=48000[orig];"
555
465
  return orig + _amix_tail(narr_vol, bgm_chain)
@@ -82,8 +82,7 @@ def windowed_volume_keyframes(keyframes, seg_start_s, seg_end_s, default_gain, n
82
82
  return volume_keyframes(selected, start, new_id)
83
83
 
84
84
 
85
- def clip_from_segment(segment=None):
86
- segment = segment or {}
85
+ def clip_from_segment(segment):
87
86
  scale = segment.get("scale") if isinstance(segment.get("scale"), dict) else {}
88
87
  position = segment.get("position") if isinstance(segment.get("position"), dict) else {}
89
88
  flip = segment.get("flip") if isinstance(segment.get("flip"), dict) else {}
@@ -229,48 +228,32 @@ def unsupported_track_note(kind):
229
228
 
230
229
 
231
230
  RESOURCE_TRACKS = {
232
- "sound": ("audios", "audio"),
233
- "sticker": ("stickers", "sticker"),
234
- "text_template": ("text_templates", "text"),
235
- "video_effect": ("video_effects", "effect"),
236
- "face_effect": ("video_effects", "effect"),
231
+ "sound": "audios",
232
+ "sticker": "stickers",
233
+ "text_template": "text_templates",
234
+ "video_effect": "video_effects",
235
+ "face_effect": "video_effects",
237
236
  }
238
237
 
239
238
 
240
- def _resource_value(config, snake_case, camel_case, default=None):
241
- if snake_case in config:
242
- return config[snake_case]
243
- return config.get(camel_case, default)
244
-
245
-
246
- def _load_resource_config(config, kind):
247
- if isinstance(config, str):
248
- with open(config, encoding="utf-8") as source:
249
- config = json.load(source)
250
- if not isinstance(config, dict):
251
- raise ValueError(f"{kind} resource_config must be an object")
252
- return config
253
-
254
-
255
239
  def _resource_material(segment, kind, new_id):
256
240
  raw = segment.get("material")
257
241
  config = segment.get("resource_config")
258
242
  if raw is not None and config is not None:
259
243
  raise ValueError(f"{kind} segment must use either material or resource_config, not both")
260
244
  if config is not None:
261
- config = _load_resource_config(config, kind)
262
- raw = _resource_value(config, "main_config", "mainConfig")
263
- if isinstance(raw, str):
264
- raw = json.loads(raw)
245
+ if not isinstance(config, dict):
246
+ raise ValueError(f"{kind} resource_config must be an object")
247
+ raw = config.get("main_config")
265
248
  if not isinstance(raw, dict):
266
249
  raise ValueError(f"{kind} resource_config.main_config must be an object")
267
250
  raw = deepcopy(raw)
268
- resource_id = _resource_value(config, "resource_id", "resourceId")
251
+ resource_id = config.get("resource_id")
269
252
  if resource_id is not None:
270
253
  raw.setdefault("resource_id", resource_id)
271
254
  if config.get("resources"):
272
255
  raw["_bundle_resources"] = deepcopy(config["resources"])
273
- cover_img = _resource_value(config, "cover_img", "coverImg")
256
+ cover_img = config.get("cover_img")
274
257
  if kind == "sticker" and cover_img:
275
258
  raw["icon_url"] = cover_img
276
259
  raw["preview_cover_url"] = cover_img
@@ -284,7 +267,7 @@ def _resource_material(segment, kind, new_id):
284
267
 
285
268
  def build_resource_track(ctx, timeline_track):
286
269
  kind = timeline_track["kind"]
287
- materials_key, _track_type = RESOURCE_TRACKS[kind]
270
+ materials_key = RESOURCE_TRACKS[kind]
288
271
  track_name = timeline_track.get("name", kind)
289
272
  for item in timeline_track.get("segments", []):
290
273
  ts, te = float(item["timeline_start"]), float(item["timeline_end"])
@@ -300,8 +283,6 @@ def build_resource_track(ctx, timeline_track):
300
283
  material = _resource_material(authored_item, kind, ctx.new_id)
301
284
  ctx.materials[materials_key].append(material)
302
285
  config = authored_item.get("resource_config") or {}
303
- if config:
304
- config = _load_resource_config(config, kind)
305
286
  if kind == "text_template":
306
287
  subordinate_resources = deepcopy(material.get("_bundle_resources", []))
307
288
  subordinate_texts = deepcopy(config.get("texts", []))
@@ -332,12 +313,10 @@ def _attachment_material(spec, kind, new_id):
332
313
  if not isinstance(spec, dict):
333
314
  raise ValueError(f"video {kind} must be an object")
334
315
  material = deepcopy(spec)
335
- config = material.pop("main_config", material.pop("mainConfig", None))
316
+ config = material.pop("main_config", None)
336
317
  resources = material.pop("resources", None)
337
- resource_id = material.pop("resourceId", None)
318
+ resource_id = material.pop("resource_id", None)
338
319
  if config is not None:
339
- if isinstance(config, str):
340
- config = json.loads(config)
341
320
  if not isinstance(config, dict):
342
321
  raise ValueError(f"video {kind}.main_config must be an object")
343
322
  merged = deepcopy(config)
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass, field
5
- from typing import Callable
5
+ from collections.abc import Callable
6
6
 
7
7
  from jianying_schema import us
8
8
  from jianying_tracks import TrackAllocator
@@ -50,20 +50,22 @@ class DraftBuildContext:
50
50
 
51
51
  @classmethod
52
52
  def from_timeline(cls, timeline, new_id, probe):
53
- canvas = timeline.get("canvas", {})
53
+ """Build from a timeline already validated by jianying_timeline_contract."""
54
+ canvas = timeline["canvas"]
54
55
  return cls(
55
- width=int(canvas.get("width", 1920)),
56
- height=int(canvas.get("height", 1080)),
57
- fps=float(canvas.get("fps", 30)),
58
- total_us=us(timeline.get("duration", 0)),
56
+ width=canvas["width"],
57
+ height=canvas["height"],
58
+ fps=float(canvas["fps"]),
59
+ total_us=us(timeline["duration"]),
59
60
  new_id=new_id,
60
61
  probe=probe,
61
- resource_packages=dict(timeline.get("resource_packages") or {}),
62
- style_presets=dict(timeline.get("style_presets") or {}),
62
+ resource_packages=timeline.get("resource_packages", {}),
63
+ style_presets=timeline.get("style_presets", {}),
63
64
  )
64
65
 
65
66
  def media_duration(self, path, fallback_us):
66
- if path and os.path.exists(path):
67
+ """(duration_us, width, height) probed from the file; images and missing files fall back."""
68
+ if os.path.exists(path):
67
69
  duration_us, width, height = self.probe(path)
68
70
  return duration_us or fallback_us, width, height
69
71
  return fallback_us, 0, 0
@@ -97,14 +99,14 @@ class DraftBuildContext:
97
99
  def finalize_tracks(self):
98
100
  # Python's stable sort preserves the timeline's authored order inside
99
101
  # one semantic band (for example narration before BGM).
100
- self.tracks.sort(key=lambda item: item.get("_layout_order", 120_000))
102
+ self.tracks.sort(key=lambda item: item["_layout_order"])
101
103
  type_counts = {}
102
104
  for track in self.tracks:
103
105
  count = type_counts.get(track["type"], 0)
104
106
  track["flag"] = 0 if count == 0 else 2
105
107
  type_counts[track["type"]] = count + 1
106
- track.pop("_semantic_kind", None)
107
- track.pop("_layout_order", None)
108
+ del track["_semantic_kind"]
109
+ del track["_layout_order"]
108
110
  return self.tracks
109
111
 
110
112
  def note(self, message):