@oh-story/dsh 0.1.10 → 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 (444) hide show
  1. package/README.md +7 -7
  2. package/lib/client.js +7 -3
  3. package/lib/client.js.map +2 -2
  4. package/lib/drama/manifest.json +212 -142
  5. package/lib/drama/skills/short-drama/SKILL.md +14 -2
  6. package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
  7. package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
  8. package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
  9. package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
  10. package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
  11. package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
  12. package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
  13. package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
  14. package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
  15. package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
  16. package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
  17. package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
  18. package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
  19. package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
  20. package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
  21. package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
  22. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
  23. package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
  24. package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
  25. package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
  26. package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
  27. package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
  28. package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
  29. package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
  30. package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
  31. package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
  32. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
  33. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
  34. package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
  35. package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
  36. package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
  37. package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
  38. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
  39. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
  40. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
  41. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
  42. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
  43. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
  44. package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
  45. package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
  46. package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
  47. package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
  48. package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
  49. package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
  50. package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
  51. package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
  52. package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
  53. package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
  54. package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
  55. package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
  56. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
  57. package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
  58. package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
  59. package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
  60. package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
  61. package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
  62. package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
  63. package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
  64. package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
  65. package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
  66. package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
  67. package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
  68. package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
  69. package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
  70. package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
  71. package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
  72. package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
  73. package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
  74. package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
  75. package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
  76. package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
  77. package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
  78. package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
  79. package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
  80. package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
  81. package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
  82. package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
  83. package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
  84. package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
  85. package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
  86. package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
  87. package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
  88. package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
  89. package/lib/index.js +71 -21
  90. package/lib/index.js.map +3 -3
  91. package/lib/oh-story/manifest.json +545 -310
  92. package/lib/oh-story/roles/chapter-extractor.md +11 -11
  93. package/lib/oh-story/roles/character-designer.md +1 -2
  94. package/lib/oh-story/roles/consistency-checker.md +11 -9
  95. package/lib/oh-story/roles/narrative-writer.md +24 -25
  96. package/lib/oh-story/roles/story-architect.md +9 -61
  97. package/lib/oh-story/roles/story-explorer.md +20 -125
  98. package/lib/oh-story/roles/story-researcher.md +0 -1
  99. package/lib/oh-story/skills/story/SKILL.md +6 -6
  100. package/lib/oh-story/skills/story/VERSION +1 -1
  101. package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
  102. package/lib/oh-story/skills/story/references/author-memory.md +40 -118
  103. package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
  104. package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
  105. package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
  106. package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
  107. package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  108. package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
  109. package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
  110. package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
  111. package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
  112. package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
  113. package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
  114. package/lib/oh-story/skills/story-import/SKILL.md +63 -579
  115. package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
  116. package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
  117. package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
  118. package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
  119. package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
  120. package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
  121. package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
  122. package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
  123. package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
  124. package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
  125. package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
  126. package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
  127. package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
  128. package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
  129. package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
  130. package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
  131. package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
  132. package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
  133. package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
  134. package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
  135. package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
  136. package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
  137. package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  138. package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  139. package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  140. package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  141. package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
  142. package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
  143. package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
  144. package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  145. package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
  146. package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
  147. package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  148. package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
  149. package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  150. package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
  151. package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
  152. package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
  153. package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
  154. package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
  155. package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
  156. package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
  157. package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  158. package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
  159. package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
  160. package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
  161. package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
  162. package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
  163. package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
  164. package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
  165. package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
  166. package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
  167. package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
  168. package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
  169. package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
  170. package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
  171. package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
  172. package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
  173. package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
  174. package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
  175. package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
  176. package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
  177. package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
  178. package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
  179. package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
  180. package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
  181. package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
  182. package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
  183. package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
  184. package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
  185. package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
  186. package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
  187. package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
  188. package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
  189. package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
  190. package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
  191. package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
  192. package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
  193. package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
  194. package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
  195. package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  196. package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
  197. package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
  198. package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
  199. package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
  200. package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
  201. package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
  202. package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
  203. package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
  204. package/lib/oh-story/skills/story-review/SKILL.md +19 -273
  205. package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
  206. package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
  207. package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
  208. package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
  209. package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
  210. package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
  211. package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
  212. package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
  213. package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
  214. package/lib/oh-story/skills/story-review/references/solo.md +52 -0
  215. package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
  216. package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
  217. package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
  218. package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
  219. package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
  220. package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
  221. package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
  222. package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
  223. package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
  224. package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
  225. package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
  226. package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  227. package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
  228. package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
  229. package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  230. package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
  231. package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
  232. package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
  233. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-catalog.md +1 -1
  234. package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
  235. package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
  236. package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
  237. package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
  238. package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
  239. package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
  240. package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
  241. package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
  242. package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
  243. package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
  244. package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
  245. package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +4 -4
  246. package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
  247. package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
  248. package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
  249. package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
  250. package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
  251. package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
  252. package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
  253. package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
  254. package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
  255. package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  256. package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
  257. package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
  258. package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
  259. package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  260. package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
  261. package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
  262. package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
  263. package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
  264. package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  265. package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
  266. package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
  267. package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
  268. package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
  269. package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
  270. package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
  271. package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
  272. package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
  273. package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
  274. package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
  275. package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
  276. package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
  277. package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
  278. package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
  279. package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
  280. package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
  281. package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
  282. package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
  283. package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
  284. package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
  285. package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
  286. package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
  287. package/lib/types/client/video-studio.d.ts.map +1 -1
  288. package/lib/types/native-hooks.d.ts +2 -2
  289. package/lib/types/native-hooks.d.ts.map +1 -1
  290. package/lib/types/role-tool.d.ts.map +1 -1
  291. package/lib/types/skill-provider.d.ts.map +1 -1
  292. package/lib/types/video-project.d.ts.map +1 -1
  293. package/lib/types/workspace-route.d.ts.map +1 -1
  294. package/lib/video-recap/CHANGELOG.md +60 -1
  295. package/lib/video-recap/README.en.md +348 -137
  296. package/lib/video-recap/README.md +340 -136
  297. package/lib/video-recap/manifest.json +406 -251
  298. package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
  299. package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
  300. package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  301. package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
  302. package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
  303. package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
  304. package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
  305. package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
  306. package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
  307. package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  308. package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  309. package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  310. package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  311. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  312. package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  313. package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
  314. package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
  315. package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
  316. package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
  317. package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
  318. package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
  319. package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
  320. package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
  321. package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  322. package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
  323. package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
  324. package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
  325. package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
  326. package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
  327. package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
  328. package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
  329. package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
  330. package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
  331. package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
  332. package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
  333. package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
  334. package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
  335. package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
  336. package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
  337. package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  338. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
  339. package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
  340. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
  341. package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  342. package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
  343. package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
  344. package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
  345. package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
  346. package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
  347. package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
  348. package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
  349. package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
  350. package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
  351. package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
  352. package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
  353. package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
  354. package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
  355. package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
  356. package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
  357. package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
  358. package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
  359. package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
  360. package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
  361. package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
  362. package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
  363. package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
  364. package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
  365. package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
  366. package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
  367. package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
  368. package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
  369. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
  370. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
  371. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
  372. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
  373. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
  374. package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
  375. package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
  376. package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
  377. package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
  378. package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
  379. package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
  380. package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
  381. package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
  382. package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
  383. package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
  384. package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
  385. package/lib/video-recap/skills/video-script/SKILL.md +16 -13
  386. package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
  387. package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
  388. package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
  389. package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
  390. package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
  391. package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
  392. package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
  393. package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
  394. package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
  395. package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
  396. package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
  397. package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
  398. package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
  399. package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
  400. package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
  401. package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
  402. package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  403. package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
  404. package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  405. package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
  406. package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
  407. package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
  408. package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
  409. package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
  410. package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
  411. package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
  412. package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
  413. package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
  414. package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
  415. package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
  416. package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
  417. package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
  418. package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
  419. package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
  420. package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
  421. package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
  422. package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  423. package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
  424. package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
  425. package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  426. package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  427. package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
  428. package/package.json +1 -1
  429. package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
  430. package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
  431. package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
  432. package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
  433. package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
  434. package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
  435. package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
  436. package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
  437. package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
  438. package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
  439. package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
  440. package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
  441. package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
  442. /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
  443. /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
  444. /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
@@ -0,0 +1,324 @@
1
+ """Strict loader for versioned, output-clock subtitle tracks.
2
+
3
+ This module intentionally uses only the Python standard library so it can be
4
+ loaded by the independently distributed ``video-assemble`` skill.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import math
11
+ from collections.abc import Mapping
12
+ from decimal import Decimal
13
+ from fractions import Fraction
14
+ from pathlib import Path
15
+
16
+
17
+ SCHEMA_VERSION = 1
18
+
19
+ # Digest keys older tracks declared; they are ignored, never a reason to fail.
20
+ _LEGACY_BINDING_KEYS = frozenset({"sha256", "edit_sha256"})
21
+ _EVIDENCE_KINDS = frozenset(
22
+ {
23
+ "human_verified",
24
+ "word_timestamps",
25
+ "asr_boundary_calibrated",
26
+ "legacy_estimate",
27
+ }
28
+ )
29
+ _CALIBRATION_KINDS = frozenset({"none", "asr_energy", "human_boundary"})
30
+ _WORD_ALIGNMENT_KINDS = frozenset({"none", "asr_words", "human_words"})
31
+
32
+
33
+ class SubtitleTrackError(ValueError):
34
+ """The subtitle track is malformed, stale, or incompatible with its caller."""
35
+
36
+
37
+ def _fail(path, message):
38
+ raise SubtitleTrackError(f"{path}: {message}")
39
+
40
+
41
+ def _mapping(value, path):
42
+ if not isinstance(value, Mapping):
43
+ _fail(path, "must be an object")
44
+ return value
45
+
46
+
47
+ def _strict_fields(value, *, required, optional=(), path):
48
+ value = _mapping(value, path)
49
+ keys = set(value)
50
+ missing = set(required) - keys
51
+ unknown = keys - set(required) - set(optional)
52
+ if missing:
53
+ _fail(path, f"missing field(s): {', '.join(sorted(missing))}")
54
+ if unknown:
55
+ _fail(path, f"unknown field(s): {', '.join(sorted(unknown))}")
56
+ return value
57
+
58
+
59
+ def _integer(value, path, *, minimum=None):
60
+ if isinstance(value, bool) or not isinstance(value, int):
61
+ _fail(path, "must be an integer")
62
+ if minimum is not None and value < minimum:
63
+ _fail(path, f"must be >= {minimum}")
64
+ return value
65
+
66
+
67
+ def _nonempty_string(value, path):
68
+ if not isinstance(value, str) or not value.strip():
69
+ _fail(path, "must be a nonempty string")
70
+ return value
71
+
72
+
73
+ def _local_path(value, path):
74
+ return str(Path(_nonempty_string(value, path)).resolve())
75
+
76
+
77
+ def _seconds_fraction(value, path):
78
+ if isinstance(value, bool):
79
+ _fail(path, "duration must be finite and numeric")
80
+ if isinstance(value, Fraction):
81
+ result = value
82
+ elif isinstance(value, Decimal):
83
+ if not value.is_finite():
84
+ _fail(path, "duration must be finite")
85
+ result = Fraction(value)
86
+ elif isinstance(value, int):
87
+ result = Fraction(value)
88
+ elif isinstance(value, float):
89
+ if not math.isfinite(value):
90
+ _fail(path, "duration must be finite")
91
+ result = Fraction(str(value))
92
+ else:
93
+ _fail(path, "duration must be an int, float, Decimal, or Fraction")
94
+ if result < 0:
95
+ _fail(path, "duration must be nonnegative")
96
+ return result
97
+
98
+
99
+ def _load_document(path_or_mapping):
100
+ if isinstance(path_or_mapping, Mapping):
101
+ return path_or_mapping
102
+ try:
103
+ path = Path(path_or_mapping)
104
+ except TypeError as exc:
105
+ raise SubtitleTrackError("track must be a mapping or filesystem path") from exc
106
+ try:
107
+ value = json.loads(path.read_text(encoding="utf-8"))
108
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
109
+ raise SubtitleTrackError(f"cannot read subtitle track {path}: {exc}") from exc
110
+ return _mapping(value, "track")
111
+
112
+
113
+ def _validate_picture_binding(binding, expected):
114
+ binding = _strict_fields(
115
+ binding, required={"path"}, optional={"edit_plan"} | _LEGACY_BINDING_KEYS,
116
+ path="bindings.picture",
117
+ )
118
+ expected = _mapping(expected, "expected_picture_identity")
119
+ picture_path = _local_path(binding["path"], "bindings.picture.path")
120
+ expected_path = _local_path(expected.get("path"), "expected_picture_identity.path")
121
+ if picture_path != expected_path:
122
+ _fail("bindings.picture.path", "picture path does not match current picture")
123
+
124
+ edit_plan = binding.get("edit_plan")
125
+ if edit_plan is not None:
126
+ edit_plan = _local_path(edit_plan, "bindings.picture.edit_plan")
127
+ if "edit_plan" not in expected:
128
+ _fail("expected_picture_identity.edit_plan", "edit plan is required by track")
129
+ expected_edit = _local_path(expected["edit_plan"], "expected_picture_identity.edit_plan")
130
+ if edit_plan != expected_edit:
131
+ _fail("bindings.picture.edit_plan", "edit plan does not match current edit")
132
+ return {"path": picture_path, **({"edit_plan": edit_plan} if edit_plan else {})}
133
+
134
+
135
+ _AUDIO_BINDING_FACTS = ("selected_stream", "sample_rate", "packet_count")
136
+
137
+
138
+ def _validate_audio_binding(binding, expected):
139
+ binding = _strict_fields(
140
+ binding, required=set(_AUDIO_BINDING_FACTS), optional=_LEGACY_BINDING_KEYS,
141
+ path="bindings.audio",
142
+ )
143
+ expected = _mapping(expected, "expected_audio_identity")
144
+ result = {}
145
+ for key in _AUDIO_BINDING_FACTS:
146
+ declared = _integer(binding[key], f"bindings.audio.{key}", minimum=0)
147
+ actual = _integer(expected.get(key), f"expected_audio_identity.{key}", minimum=0)
148
+ if declared != actual:
149
+ _fail(f"bindings.audio.{key}", f"{key} does not match adopted audio")
150
+ result[key] = declared
151
+ return result
152
+
153
+
154
+ def _validate_evidence(value, cue_path):
155
+ path = f"{cue_path}.timing_evidence"
156
+ value = _strict_fields(
157
+ value,
158
+ required={"kind", "evidence_refs", "calibration", "word_alignment"},
159
+ path=path,
160
+ )
161
+ kind = _nonempty_string(value["kind"], f"{path}.kind")
162
+ calibration = _nonempty_string(value["calibration"], f"{path}.calibration")
163
+ word_alignment = _nonempty_string(
164
+ value["word_alignment"], f"{path}.word_alignment"
165
+ )
166
+ if kind not in _EVIDENCE_KINDS:
167
+ _fail(path, f"timing_evidence kind is not supported: {kind!r}")
168
+ if calibration not in _CALIBRATION_KINDS:
169
+ _fail(path, f"timing_evidence calibration is not supported: {calibration!r}")
170
+ if word_alignment not in _WORD_ALIGNMENT_KINDS:
171
+ _fail(path, f"timing_evidence word_alignment is not supported: {word_alignment!r}")
172
+ refs = value["evidence_refs"]
173
+ if not isinstance(refs, list):
174
+ _fail(path, "timing_evidence evidence_refs must be a list")
175
+ refs = [_nonempty_string(ref, f"{path}.evidence_refs[{index}]") for index, ref in enumerate(refs)]
176
+
177
+ valid_shape = {
178
+ "legacy_estimate": calibration == "none" and word_alignment == "none",
179
+ "asr_boundary_calibrated": calibration == "asr_energy" and word_alignment == "none",
180
+ "word_timestamps": calibration in {"none", "asr_energy"} and word_alignment == "asr_words",
181
+ "human_verified": calibration == "human_boundary" and word_alignment in {"none", "human_words"},
182
+ }[kind]
183
+ if not valid_shape:
184
+ _fail(path, f"timing_evidence fields are inconsistent with kind {kind!r}")
185
+ if kind != "legacy_estimate" and not refs:
186
+ _fail(path, f"timing_evidence kind {kind!r} requires evidence_refs")
187
+ return {
188
+ "kind": kind,
189
+ "evidence_refs": refs,
190
+ "calibration": calibration,
191
+ "word_alignment": word_alignment,
192
+ }
193
+
194
+
195
+ def load_subtitle_track(
196
+ path_or_mapping,
197
+ *,
198
+ expected_picture_identity,
199
+ expected_audio_identity,
200
+ expected_duration_seconds,
201
+ reject_legacy_estimate=False,
202
+ ):
203
+ """Validate schema v1 and return metadata plus second-based render entries.
204
+
205
+ Identity arguments are current facts (paths, stream index, sample rate,
206
+ packet count) supplied independently by the caller; declarations inside the
207
+ track are compared against them, never accepted on their own. Cue boundaries
208
+ and text are validated, not split or corrected.
209
+ """
210
+
211
+ if not isinstance(reject_legacy_estimate, bool):
212
+ _fail("reject_legacy_estimate", "must be a boolean")
213
+ track = _strict_fields(
214
+ _load_document(path_or_mapping),
215
+ required={"schema_version", "clock", "overlap_policy", "bindings", "cues"},
216
+ path="track",
217
+ )
218
+ if type(track["schema_version"]) is not int or track["schema_version"] != SCHEMA_VERSION:
219
+ _fail(
220
+ "schema_version",
221
+ f"only schema_version {SCHEMA_VERSION} is supported; got {track['schema_version']!r}",
222
+ )
223
+ if track["overlap_policy"] != "forbid":
224
+ _fail("overlap_policy", "schema v1 requires fail-closed value 'forbid'")
225
+
226
+ clock = _strict_fields(
227
+ track["clock"],
228
+ required={"kind", "timebase", "duration_ticks"},
229
+ path="clock",
230
+ )
231
+ if clock["kind"] != "output":
232
+ _fail("clock.kind", "schema v1 supports output clock only")
233
+ timebase = _strict_fields(
234
+ clock["timebase"], required={"numerator", "denominator"}, path="clock.timebase"
235
+ )
236
+ numerator = _integer(timebase["numerator"], "clock.timebase.numerator", minimum=1)
237
+ denominator = _integer(timebase["denominator"], "clock.timebase.denominator", minimum=1)
238
+ duration_ticks = _integer(clock["duration_ticks"], "clock.duration_ticks", minimum=0)
239
+ tick_seconds = Fraction(numerator, denominator)
240
+ duration = duration_ticks * tick_seconds
241
+ expected_duration = _seconds_fraction(expected_duration_seconds, "expected_duration_seconds")
242
+ if duration != expected_duration:
243
+ _fail(
244
+ "clock.duration_ticks",
245
+ f"duration {duration} does not match independently supplied duration {expected_duration}",
246
+ )
247
+ try:
248
+ duration_seconds = float(duration)
249
+ except OverflowError:
250
+ _fail("clock.duration_ticks", "duration must be representable as finite renderer seconds")
251
+ if not math.isfinite(duration_seconds):
252
+ _fail("clock.duration_ticks", "duration must be representable as finite renderer seconds")
253
+
254
+ bindings = _strict_fields(
255
+ track["bindings"], required={"picture", "audio"}, path="bindings"
256
+ )
257
+ picture = _validate_picture_binding(bindings["picture"], expected_picture_identity)
258
+ audio = _validate_audio_binding(bindings["audio"], expected_audio_identity)
259
+
260
+ cues = track["cues"]
261
+ if not isinstance(cues, list):
262
+ _fail("cues", "must be a list")
263
+ entries = []
264
+ kinds = set()
265
+ previous_end = 0
266
+ for index, raw_cue in enumerate(cues):
267
+ cue_path = f"cues[{index}]"
268
+ cue = _strict_fields(
269
+ raw_cue,
270
+ required={"start_tick", "end_tick", "text", "attribution", "timing_evidence"},
271
+ path=cue_path,
272
+ )
273
+ start_tick = _integer(cue["start_tick"], f"{cue_path}.start_tick", minimum=0)
274
+ end_tick = _integer(cue["end_tick"], f"{cue_path}.end_tick", minimum=0)
275
+ if end_tick <= start_tick:
276
+ _fail(cue_path, "half-open cue requires end_tick > start_tick")
277
+ if end_tick > duration_ticks:
278
+ _fail(cue_path, "cue end_tick exceeds output duration")
279
+ if start_tick < previous_end:
280
+ _fail(cue_path, "cue is out of order or overlaps the previous cue")
281
+ previous_end = end_tick
282
+ text = _nonempty_string(cue["text"], f"{cue_path}.text")
283
+
284
+ attribution = _strict_fields(
285
+ cue["attribution"], required={"kind", "ref"}, path=f"{cue_path}.attribution"
286
+ )
287
+ source = _nonempty_string(attribution["kind"], f"{cue_path}.attribution.kind")
288
+ if source not in {"source", "narration"}:
289
+ _fail(f"{cue_path}.attribution.kind", "must be 'source' or 'narration'")
290
+ source_ref = _nonempty_string(attribution["ref"], f"{cue_path}.attribution.ref")
291
+ evidence = _validate_evidence(cue["timing_evidence"], cue_path)
292
+ if reject_legacy_estimate and evidence["kind"] == "legacy_estimate":
293
+ _fail(cue_path, "legacy_estimate is rejected by strict caller policy")
294
+ start_seconds = float(start_tick * tick_seconds)
295
+ end_seconds = float(end_tick * tick_seconds)
296
+ if not (math.isfinite(start_seconds) and math.isfinite(end_seconds)):
297
+ _fail(cue_path, "cue float projection must be finite")
298
+ if end_seconds <= start_seconds:
299
+ _fail(cue_path, "distinct cue ticks collapse in renderer float projection")
300
+ kinds.add(evidence["kind"])
301
+ entries.append(
302
+ {
303
+ "start": start_seconds,
304
+ "end": end_seconds,
305
+ "text": text,
306
+ "source": source,
307
+ "source_ref": source_ref,
308
+ "timing_evidence": evidence,
309
+ }
310
+ )
311
+
312
+ metadata = {
313
+ "schema_version": SCHEMA_VERSION,
314
+ "clock": {
315
+ "kind": "output",
316
+ "timebase": {"numerator": numerator, "denominator": denominator},
317
+ "duration_ticks": duration_ticks,
318
+ "duration_seconds": duration_seconds,
319
+ },
320
+ "overlap_policy": "forbid",
321
+ "bindings": {"picture": picture, "audio": audio},
322
+ "timing_evidence_kinds": sorted(kinds),
323
+ }
324
+ return {"metadata": metadata, "entries": entries}
@@ -0,0 +1,200 @@
1
+ """Bind an explicit output-clock subtitle track to media before any consumer uses it.
2
+
3
+ The first render integration deliberately supports adopted AAC only. A future
4
+ newly mixed narration track needs its own final-mix identity, not this input's
5
+ soundtrack. Bound cue timing is a declared decision, not proof of speech onset.
6
+ """
7
+
8
+ import bisect
9
+ import json
10
+ from fractions import Fraction
11
+ from pathlib import Path
12
+
13
+ from artifacts import file_identity
14
+ from adoption.frozen_audio import probe_audio_packets
15
+ from lib import run_cmd
16
+ from subtitles.track import load_subtitle_track
17
+
18
+ TRACK = 'subtitle_track.json'
19
+ VALIDATION = 'subtitle_track_validation.json'
20
+ VALIDATION_SCHEMA = 1
21
+ PROJECTOR_VERSION = 1
22
+
23
+
24
+ def current_bindings(video, selected_audio_stream=0, *, edit_plan_path=None):
25
+ """Compute the binding facts independently of the supplied subtitle track.
26
+
27
+ The picture binding is the resolved media path (plus the edit plan path when one
28
+ exists); the audio binding is the selected stream ordinal with its sample rate and
29
+ packet count.
30
+ """
31
+ packets = probe_audio_packets(video, selected_audio_stream)
32
+ picture = {'path': str(Path(video).resolve())}
33
+ if edit_plan_path is not None:
34
+ picture['edit_plan'] = str(Path(edit_plan_path).resolve())
35
+ return {'picture': picture,
36
+ 'audio': {'selected_stream': selected_audio_stream,
37
+ 'sample_rate': packets['sample_rate'],
38
+ 'packet_count': packets['packet_count']}}
39
+
40
+
41
+ def _picture_clock(video):
42
+ result = run_cmd([
43
+ 'ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_streams', '-show_frames',
44
+ '-show_entries', 'stream=time_base,start_pts,duration_ts:frame=pts', '-of', 'json', str(video),
45
+ ])
46
+ if result.returncode:
47
+ raise ValueError(f'Cannot verify subtitle picture clock: {result.stderr}')
48
+ data = json.loads(result.stdout)
49
+ stream = data['streams'][0]
50
+ timebase = Fraction(stream['time_base'])
51
+ if int(stream['start_pts']) != 0:
52
+ raise ValueError('Bound subtitle render requires an output-clock video starting at zero')
53
+ duration = int(stream['duration_ts']) * timebase
54
+ pts = [int(frame['pts']) * timebase for frame in data['frames']]
55
+ if not pts or pts[0] != 0 or any(b <= a for a, b in zip(pts, pts[1:])) or pts[-1] >= duration:
56
+ raise ValueError('Cannot verify picture frame clock: expected strictly increasing PTS within duration')
57
+ return pts, duration
58
+
59
+
60
+ def _project_boundary(target, frame_pts, duration):
61
+ """Choose an ASS centisecond that changes on the first frame at/after a cue.
62
+
63
+ ASS's 10ms clock is coarser than most frame clocks and can otherwise round a cue
64
+ forward by one frame. Do not silently degrade if two relevant frames cannot be
65
+ separated in that clock.
66
+ libass receives presentation time in integer milliseconds from ffmpeg.
67
+ """
68
+ index = bisect.bisect_left(frame_pts, target)
69
+ chosen = frame_pts[index] if index < len(frame_pts) else duration
70
+ centiseconds = chosen * 100 // 1
71
+ threshold_ms = int(centiseconds) * 10
72
+ previous_ms = int(frame_pts[index - 1] * 1000) if index else -1
73
+ if threshold_ms <= previous_ms:
74
+ raise ValueError('ASS 10ms clock cannot represent this cue boundary; use a frame-capable renderer')
75
+ ass_time = f'{centiseconds // 360000}:{centiseconds // 6000 % 60:02d}:{centiseconds // 100 % 60:02d}.{centiseconds % 100:02d}'
76
+ return {'requested_time': str(target), 'frame_index': index, 'pts': str(chosen),
77
+ 'delta': str(chosen - target), 'ass_time': ass_time}
78
+
79
+
80
+ def prepare_subtitle_track(input_video, work_dir, video_duration, *, audio_mode,
81
+ selected_audio_stream=0, edit_plan_path=None,
82
+ reject_legacy_estimate=False):
83
+ """Validate and project an explicit track before SRT/ASS/timeline/QC consumers.
84
+
85
+ No track preserves legacy behavior. A present but invalid track fails instead
86
+ of falling back to proportional timing. The persisted record is not approval.
87
+ """
88
+ work = Path(work_dir)
89
+ path = work / TRACK
90
+ validation = work / VALIDATION
91
+ validation.unlink(missing_ok=True)
92
+ if not path.exists():
93
+ return None
94
+ if audio_mode != 'adopted-packet-copy':
95
+ raise ValueError('Explicit subtitle_track currently requires adopted-packet-copy; new mix is not bound')
96
+ document = json.loads(path.read_bytes())
97
+ bindings = current_bindings(input_video, selected_audio_stream, edit_plan_path=edit_plan_path)
98
+ frame_pts, duration = _picture_clock(input_video)
99
+ if abs(float(duration) - float(video_duration)) > 0.05:
100
+ raise ValueError('Picture duration differs from assembly duration')
101
+ loaded = load_subtitle_track(
102
+ document, expected_picture_identity=bindings['picture'], expected_audio_identity=bindings['audio'],
103
+ expected_duration_seconds=duration, reject_legacy_estimate=reject_legacy_estimate,
104
+ )
105
+ clock = document['clock']['timebase']
106
+ tick = Fraction(clock['numerator'], clock['denominator'])
107
+ for entry, cue in zip(loaded['entries'], document['cues']):
108
+ start = _project_boundary(cue['start_tick'] * tick, frame_pts, duration)
109
+ end = _project_boundary(cue['end_tick'] * tick, frame_pts, duration)
110
+ if end['frame_index'] <= start['frame_index']:
111
+ raise ValueError('Subtitle cue has no visible frame in the actual picture clock')
112
+ entry.update(_bound_track=True, ass_start=start['ass_time'], ass_end=end['ass_time'],
113
+ start=float(Fraction(start['pts'])), end=float(Fraction(end['pts'])))
114
+ entry['frame_projection'] = {'requested_ticks': [cue['start_tick'], cue['end_tick']],
115
+ 'start': start, 'end': end}
116
+ loaded['validation_schema'] = VALIDATION_SCHEMA
117
+ loaded['projector_version'] = PROJECTOR_VERSION
118
+ loaded['binding'] = {
119
+ 'input_video': str(Path(input_video).resolve()), 'identities': bindings,
120
+ 'inputs': {'track': file_identity(path), 'video': file_identity(input_video),
121
+ 'edit_plan': file_identity(edit_plan_path) if edit_plan_path is not None else None},
122
+ 'duration': str(duration), 'frame_count': len(frame_pts),
123
+ 'edit_plan': str(Path(edit_plan_path).resolve()) if edit_plan_path is not None else None,
124
+ 'verification': 'media_binding_and_declared_frame_projection_only',
125
+ 'direct_listening': 'NOT_CHECKED', 'acoustic_alignment': 'NOT_CHECKED',
126
+ }
127
+ validation.write_text(json.dumps(loaded, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
128
+ return loaded
129
+
130
+
131
+ def _load_validation(work):
132
+ path = Path(work) / VALIDATION
133
+ if not path.exists():
134
+ raise ValueError('Explicit subtitle track must be prepared against current media before use')
135
+ loaded = json.loads(path.read_text(encoding='utf-8'))
136
+ if (type(loaded.get('validation_schema')) is not int or loaded['validation_schema'] != VALIDATION_SCHEMA
137
+ or type(loaded.get('projector_version')) is not int or loaded['projector_version'] != PROJECTOR_VERSION):
138
+ raise ValueError('Subtitle validation/projector version changed; prepare again')
139
+ return loaded
140
+
141
+
142
+ def bound_subtitle_entries(work_dir, video_duration):
143
+ """Return prepared exact entries, or None only when no explicit track exists."""
144
+ if work_dir is None:
145
+ return None
146
+ work = Path(work_dir)
147
+ if not (work / TRACK).exists():
148
+ return None
149
+ loaded = _load_validation(work)
150
+ record = loaded['binding']
151
+ inputs = record['inputs']
152
+ if file_identity(work / TRACK) != inputs['track']:
153
+ raise ValueError('stale subtitle track: author file changed after prepare')
154
+ video = Path(record['input_video'])
155
+ if not video.is_file() or file_identity(video) != inputs['video']:
156
+ raise ValueError('stale subtitle track: adopted media changed after prepare')
157
+ if record['edit_plan'] and (not Path(record['edit_plan']).is_file()
158
+ or file_identity(record['edit_plan']) != inputs['edit_plan']):
159
+ raise ValueError('stale subtitle track: edit plan changed after prepare')
160
+ if abs(float(Fraction(record['duration'])) - float(video_duration)) > 0.05:
161
+ raise ValueError('stale subtitle track: consumer duration changed after prepare')
162
+ return loaded['entries']
163
+
164
+
165
+ def verify_rendered_picture(work_dir, output_path):
166
+ """Check the actual rendered frame clock, not just pre-render declarations."""
167
+ work = Path(work_dir)
168
+ if not (work / TRACK).exists():
169
+ return None
170
+ path = work / VALIDATION
171
+ loaded = json.loads(path.read_text(encoding='utf-8'))
172
+ bound_subtitle_entries(work, float(Fraction(loaded['binding']['duration'])))
173
+ loaded['rendered_picture'] = {'output': str(Path(output_path).resolve()),
174
+ 'frame_clock_verified': False}
175
+ path.write_text(json.dumps(loaded, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
176
+ if _picture_clock(output_path) != _picture_clock(loaded['binding']['input_video']):
177
+ raise ValueError('Rendered frame clock changed; subtitle frame projection is no longer valid')
178
+ loaded['rendered_picture'].update(frame_clock_verified=True,
179
+ frame_count=loaded['binding']['frame_count'],
180
+ identity=file_identity(output_path))
181
+ path.write_text(json.dumps(loaded, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
182
+ return loaded['rendered_picture']
183
+
184
+
185
+ def manifest_subtitle_evidence(work_dir, input_video, output_path):
186
+ """Only expose evidence for the actual input and verified output of this render."""
187
+ work = Path(work_dir)
188
+ if not (work / TRACK).exists():
189
+ return None
190
+ loaded = _load_validation(work)
191
+ bound_subtitle_entries(work, float(Fraction(loaded['binding']['duration'])))
192
+ rendered = loaded.get('rendered_picture', {})
193
+ if rendered.get('frame_clock_verified') is not True:
194
+ raise ValueError('Subtitle output frame clock has not been verified')
195
+ if str(Path(input_video).resolve()) != loaded['binding']['input_video']:
196
+ raise ValueError('stale subtitle manifest: input media differs')
197
+ if not Path(output_path).is_file() or file_identity(output_path) != rendered.get('identity'):
198
+ raise ValueError('stale subtitle manifest: actual output differs from verified render')
199
+ return {'validation_path': str((work / VALIDATION).resolve()),
200
+ 'binding': loaded['binding'], 'metadata': loaded['metadata'], 'rendered_picture': rendered}
@@ -36,6 +36,12 @@ def _ceil_time(value, digits=4):
36
36
  return math.ceil((float(value) * scale) - 1e-9) / scale
37
37
 
38
38
 
39
+ def _floor_time(value, digits=4):
40
+ """Round an interval start outward so serialization cannot clip source samples."""
41
+ scale = 10 ** digits
42
+ return math.floor((float(value) * scale) + 1e-9) / scale
43
+
44
+
39
45
  def build_timeline(canvas, duration_s, video_clips, narration_segments,
40
46
  bgm=None, ducking=None, subtitle_segments=None,
41
47
  image_segments=(), resource_packages=None,
@@ -48,15 +54,17 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
48
54
  "timeline_start", "timeline_end"}] (cut mode: one per clip;
49
55
  full mode: a single clip spanning the whole video).
50
56
  narration_segments: placed beats [{"source_path", "timeline_start",
51
- "timeline_end", "text", "overlaps_speech", "gain"?}]; a zero-width
57
+ "timeline_end", "text", "overlaps_speech", "gain"}]; a zero-width
52
58
  beat (an unplaced segment) is skipped.
53
59
  subtitle_segments: optional display-ready text cues [{"text", "timeline_start",
54
60
  "timeline_end"}]. When present, this is authoritative for the
55
61
  subtitle/text track; narration segment text remains raw editor metadata.
56
- bgm: optional {"source_path", "volume", "ducking_volume"}.
57
- ducking: {"idle", "speech", "quiet", "fade", "bridge"?} for the original-audio
62
+ bgm: optional {"source_path", "volume", "ducking_volume", "fade"}.
63
+ ducking: {"idle", "speech", "quiet", "fade", "bridge"} for the original-audio
58
64
  automation; None disables original ducking (flat original). `bridge` holds
59
- the duck across inter-beat gaps shorter than it (defaults to 2*fade).
65
+ the duck across inter-beat gaps shorter than it.
66
+ image_segments: optional v2 local image overlays [{"source_path", "timeline_start",
67
+ "timeline_end", ...authoring extensions}], passed through as authored.
60
68
  """
61
69
  placed = [
62
70
  s for s in narration_segments
@@ -71,7 +79,7 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
71
79
  restore_at = max(
72
80
  hold_end, float(s.get("source_restore_at", hold_end + float(ducking["fade"])))
73
81
  )
74
- level = float(ducking["speech" if s.get("overlaps_speech", True) else "quiet"])
82
+ level = float(ducking["speech" if s["overlaps_speech"] else "quiet"])
75
83
  duck_windows.append((float(s["timeline_start"]), hold_end, level, restore_at))
76
84
 
77
85
  # --- video track: each clip carries its original audio + ducking automation
@@ -82,7 +90,7 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
82
90
  if ducking is not None:
83
91
  audio["volume_keyframes"] = release_ducking_keyframes(
84
92
  duck_windows, ducking["idle"], ducking["fade"], ts, te,
85
- bridge=ducking.get("bridge"))
93
+ bridge=ducking["bridge"])
86
94
  audio["base_gain"] = round(float(ducking["idle"]), 4)
87
95
  else:
88
96
  audio["base_gain"] = 1.0
@@ -110,11 +118,11 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
110
118
  for s in placed:
111
119
  narration = {
112
120
  "source_path": s["source_path"],
113
- "timeline_start": round(float(s["timeline_start"]), 4),
121
+ "timeline_start": _floor_time(s["timeline_start"], 4),
114
122
  "timeline_end": _ceil_time(s["timeline_end"], 4),
115
- "gain": round(float(s.get("gain", 1.0)), 4),
116
- "text": s.get("text", ""),
117
- "overlaps_speech": bool(s.get("overlaps_speech", True)),
123
+ "gain": round(float(s["gain"]), 4),
124
+ "text": s["text"],
125
+ "overlaps_speech": bool(s["overlaps_speech"]),
118
126
  }
119
127
  for key in ("source_duck_end", "source_restore_at", "source_handoff_status", "source_entry_status"):
120
128
  if key in s:
@@ -127,12 +135,12 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
127
135
  "segments": narr_segs})
128
136
 
129
137
  # --- bgm track (optional, looped, ducked under narration)
130
- if bgm and bgm.get("source_path"):
131
- base = float(bgm.get("volume", 0.18))
132
- duck = float(bgm.get("ducking_volume", 0.10))
133
- fade = float(bgm.get("fade", (ducking or {}).get("fade", 0.25)))
138
+ if bgm:
139
+ base = float(bgm["volume"])
140
+ duck = float(bgm["ducking_volume"])
141
+ fade = float(bgm["fade"])
134
142
  kfs = ducking_keyframes(windows, base, duck, fade, 0.0, duration_s,
135
- bridge=(ducking or {}).get("bridge"))
143
+ bridge=ducking["bridge"] if ducking else None)
136
144
  tracks.append({
137
145
  "kind": "audio", "name": "bgm", "role": "bgm", "loop": True,
138
146
  "segments": [{
@@ -165,41 +173,17 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
165
173
  if text_segs:
166
174
  tracks.append({"kind": "text", "name": "subtitle", "segments": text_segs})
167
175
 
168
- # --- local image overlays (optional, timeline schema v2)
176
+ # --- local image overlays (optional, timeline schema v2). Transform fields are
177
+ # optional authoring extensions; the JianYing exporter validates and defaults them.
169
178
  images = []
170
179
  for segment in image_segments:
171
- if not isinstance(segment, dict) or not segment.get("source_path"):
172
- continue
173
- try:
174
- ts = max(0.0, float(segment["timeline_start"]))
175
- te = min(float(duration_s), float(segment["timeline_end"]))
176
- except (KeyError, TypeError, ValueError):
177
- continue
178
- if not math.isfinite(ts) or not math.isfinite(te) or te <= ts:
179
- continue
180
- scale = segment.get("scale") if isinstance(segment.get("scale"), dict) else {}
181
- position = segment.get("position") if isinstance(segment.get("position"), dict) else {}
182
- flip = segment.get("flip") if isinstance(segment.get("flip"), dict) else {}
183
180
  image = {
184
- "source_path": str(segment["source_path"]),
185
- "timeline_start": round(ts, 4),
186
- "timeline_end": round(te, 4),
187
- "opacity": round(max(0.0, min(1.0, float(segment.get("opacity", 1.0)))), 4),
188
- "rotation_degrees": round(float(segment.get("rotation_degrees", 0.0)), 4),
189
- "scale": {
190
- "x": round(float(scale.get("x", 1.0)), 4),
191
- "y": round(float(scale.get("y", 1.0)), 4),
192
- },
193
- "position": {
194
- "x": round(float(position.get("x", 0.0)), 4),
195
- "y": round(float(position.get("y", 0.0)), 4),
196
- },
197
- "flip": {
198
- "horizontal": bool(flip.get("horizontal", False)),
199
- "vertical": bool(flip.get("vertical", False)),
200
- },
181
+ "source_path": segment["source_path"],
182
+ "timeline_start": round(float(segment["timeline_start"]), 4),
183
+ "timeline_end": round(float(segment["timeline_end"]), 4),
201
184
  }
202
- for key in ("lut", "mask", "speed", "transition"):
185
+ for key in ("flip", "lut", "mask", "opacity", "position", "rotation_degrees",
186
+ "scale", "speed", "transition"):
203
187
  if key in segment:
204
188
  image[key] = deepcopy(segment[key])
205
189
  images.append(image)