videobook 0.0.0-stage → 0.2.0

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 (565) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +195 -2
  4. package/bin/videobook.js +7 -0
  5. package/dist/acestep/client.js +203 -0
  6. package/dist/acestep/config.js +62 -0
  7. package/dist/acestep/errors.js +8 -0
  8. package/dist/acestep/process.js +175 -0
  9. package/dist/alive-captions/analyze.js +117 -0
  10. package/dist/alive-captions/audio.js +179 -0
  11. package/dist/alive-captions/frames.js +59 -0
  12. package/dist/alive-captions/model.js +37 -0
  13. package/dist/alive-captions/schema.js +49 -0
  14. package/dist/alive-captions/track.js +9 -0
  15. package/dist/api-key.js +33 -0
  16. package/dist/app-state.js +56 -0
  17. package/dist/app.js +257 -0
  18. package/dist/archive-b2.js +332 -0
  19. package/dist/archive-store.js +169 -0
  20. package/dist/artifact-label.js +36 -0
  21. package/dist/artifact-refs.js +49 -0
  22. package/dist/asset-tag-extraction.js +169 -0
  23. package/dist/asset-tags.js +228 -0
  24. package/dist/asset-usage.js +23 -0
  25. package/dist/audio-cue.js +71 -0
  26. package/dist/audio-encoding.js +32 -0
  27. package/dist/audio-sync-request.js +1 -0
  28. package/dist/audio-sync.js +223 -0
  29. package/dist/audio-trim-range.js +31 -0
  30. package/dist/book-context.js +31 -0
  31. package/dist/book-manager.js +374 -0
  32. package/dist/catalog-gc.js +26 -0
  33. package/dist/chain-executor.js +815 -0
  34. package/dist/chain-generation.js +115 -0
  35. package/dist/chain-plan.js +746 -0
  36. package/dist/chain-step-review.js +60 -0
  37. package/dist/character-name.js +25 -0
  38. package/dist/chat-generation-routing.js +73 -0
  39. package/dist/chat-generation-settings.js +78 -0
  40. package/dist/chat-models.js +44 -0
  41. package/dist/chat-prompt-rewrite.js +67 -0
  42. package/dist/chat-router.js +628 -0
  43. package/dist/chat-view-actions.js +49 -0
  44. package/dist/cli/args.js +242 -0
  45. package/dist/cli/config-command.js +103 -0
  46. package/dist/cli/doctor-command.js +289 -0
  47. package/dist/cli/entry.js +2 -0
  48. package/dist/cli/key-report.js +16 -0
  49. package/dist/cli/main.js +104 -0
  50. package/dist/cli/mcp-command.js +88 -0
  51. package/dist/cli/mcp-relay.js +51 -0
  52. package/dist/cli/open-browser.js +14 -0
  53. package/dist/cli/run-command.js +231 -0
  54. package/dist/cli/serve-command.js +62 -0
  55. package/dist/cli/setup-command.js +60 -0
  56. package/dist/cli/stdio-guard.js +8 -0
  57. package/dist/cold-open-model.js +7 -0
  58. package/dist/cold-open.js +79 -0
  59. package/dist/command-reference.js +95 -0
  60. package/dist/content-store.js +74 -0
  61. package/dist/cost.js +8 -0
  62. package/dist/crop-geometry.js +23 -0
  63. package/dist/dancing-workflow.js +447 -0
  64. package/dist/data-home.js +44 -0
  65. package/dist/diagnostics.js +109 -0
  66. package/dist/dialog-quality.js +34 -0
  67. package/dist/downloads-dest.js +56 -0
  68. package/dist/editing-moments.js +194 -0
  69. package/dist/elevenlabs-audio-extension.js +66 -0
  70. package/dist/engine.js +54 -0
  71. package/dist/env-file.js +91 -0
  72. package/dist/env-secrets.js +148 -0
  73. package/dist/ffmpeg-binary.js +12 -0
  74. package/dist/ffmpeg.js +425 -0
  75. package/dist/ffprobe-binary.js +77 -0
  76. package/dist/forget-asset.js +216 -0
  77. package/dist/frame-utils.js +27 -0
  78. package/dist/generation-credit-error.js +11 -0
  79. package/dist/generation-input-mentions.js +117 -0
  80. package/dist/generation-input-policy.js +68 -0
  81. package/dist/generation-reference-mentions.js +97 -0
  82. package/dist/goal-characters.js +49 -0
  83. package/dist/goal-plan-v3.js +273 -0
  84. package/dist/goal-plan.js +220 -0
  85. package/dist/goal-planner-prompt.js +28 -0
  86. package/dist/goal-prompt-budget.js +4 -0
  87. package/dist/goal-reference-images.js +25 -0
  88. package/dist/goal-state.js +261 -0
  89. package/dist/goal-stills.js +153 -0
  90. package/dist/goal-store.js +144 -0
  91. package/dist/goal-styles.js +144 -0
  92. package/dist/goal-v3-runtime.js +157 -0
  93. package/dist/guides/character_schema_guide.md +70 -0
  94. package/dist/guides/gpt_image_2_5_prompting_guide.md +21 -0
  95. package/dist/guides/grok_imagine_prompting_guide.md +72 -0
  96. package/dist/guides/image_reproduction_guide.md +84 -0
  97. package/dist/guides/kling_3_0_prompting_guide.md +136 -0
  98. package/dist/guides/nano_banana_pro_prompting_guide.md +28 -0
  99. package/dist/guides/seedance_2_prompting_guide.md +172 -0
  100. package/dist/http-access.js +49 -0
  101. package/dist/http-byte-range.js +31 -0
  102. package/dist/hydration.js +56 -0
  103. package/dist/image-compress.js +77 -0
  104. package/dist/image-edit-default.js +1 -0
  105. package/dist/image-edit-models.js +41 -0
  106. package/dist/image-edit-settings.js +11 -0
  107. package/dist/image-region-edit.js +32 -0
  108. package/dist/image-subject-map.js +523 -0
  109. package/dist/image.js +46 -0
  110. package/dist/index.js +12 -0
  111. package/dist/job-deadline.js +60 -0
  112. package/dist/jobs/_alibaba-upload.js +30 -0
  113. package/dist/jobs/_audio-chunks.js +31 -0
  114. package/dist/jobs/_download-into-asset.js +15 -0
  115. package/dist/jobs/_fal-shared.js +22 -0
  116. package/dist/jobs/_fal-upload.js +35 -0
  117. package/dist/jobs/_image-edit-adapter.js +213 -0
  118. package/dist/jobs/_kie-poll.js +119 -0
  119. package/dist/jobs/_kie-upload.js +32 -0
  120. package/dist/jobs/_lease.js +163 -0
  121. package/dist/jobs/_persist-output.js +19 -0
  122. package/dist/jobs/_storage-retry.js +21 -0
  123. package/dist/jobs/_transcribe.js +245 -0
  124. package/dist/jobs/_translate-transcript.js +270 -0
  125. package/dist/jobs/analyze-alive-captions.js +10 -0
  126. package/dist/jobs/analyze-editing-moments.js +104 -0
  127. package/dist/jobs/analyze-jokes.js +143 -0
  128. package/dist/jobs/analyze-keyword-placement.js +155 -0
  129. package/dist/jobs/analyze-music.js +14 -0
  130. package/dist/jobs/analyze-video.js +282 -0
  131. package/dist/jobs/apply-cuts.js +83 -0
  132. package/dist/jobs/apply-sfx.js +107 -0
  133. package/dist/jobs/archive-clip.js +458 -0
  134. package/dist/jobs/assemble-goal.js +98 -0
  135. package/dist/jobs/audio-boost.js +71 -0
  136. package/dist/jobs/audio-mix.js +161 -0
  137. package/dist/jobs/backfill-asset-tags.js +144 -0
  138. package/dist/jobs/burn-watermark.js +316 -0
  139. package/dist/jobs/change-video-speed.js +176 -0
  140. package/dist/jobs/clean-video-audio.js +207 -0
  141. package/dist/jobs/cold-open.js +166 -0
  142. package/dist/jobs/combine-goal.js +108 -0
  143. package/dist/jobs/complete-provider-task.js +185 -0
  144. package/dist/jobs/copy-library-asset.js +33 -0
  145. package/dist/jobs/cost-estimate-assessment.js +113 -0
  146. package/dist/jobs/crop-image.js +47 -0
  147. package/dist/jobs/crop-video.js +71 -0
  148. package/dist/jobs/describe-image.js +181 -0
  149. package/dist/jobs/detect-scenes.js +33 -0
  150. package/dist/jobs/doctor-fix.js +100 -0
  151. package/dist/jobs/download-video.js +427 -0
  152. package/dist/jobs/enqueue-search-index.js +24 -0
  153. package/dist/jobs/even-split.js +122 -0
  154. package/dist/jobs/export-sequence.js +365 -0
  155. package/dist/jobs/extend-audio.js +229 -0
  156. package/dist/jobs/extract-audio.js +72 -0
  157. package/dist/jobs/extract-frame.js +58 -0
  158. package/dist/jobs/extract-notebook-audio.js +82 -0
  159. package/dist/jobs/extract-notebook-frame.js +93 -0
  160. package/dist/jobs/extract-video-clip.js +8 -0
  161. package/dist/jobs/extract-video-poster.js +55 -0
  162. package/dist/jobs/extract-waveform.js +25 -0
  163. package/dist/jobs/finish-goal.js +255 -0
  164. package/dist/jobs/forget-asset.js +11 -0
  165. package/dist/jobs/generate-acestep-cover.js +219 -0
  166. package/dist/jobs/generate-media.js +70 -0
  167. package/dist/jobs/image-subjects.js +1059 -0
  168. package/dist/jobs/index.js +18 -0
  169. package/dist/jobs/inject-first-frame.js +175 -0
  170. package/dist/jobs/isolate-vocals.js +69 -0
  171. package/dist/jobs/kie-paygate.js +177 -0
  172. package/dist/jobs/label-music-sections.js +5 -0
  173. package/dist/jobs/osg-placement.js +135 -0
  174. package/dist/jobs/pad-video.js +72 -0
  175. package/dist/jobs/plan-goal.js +97 -0
  176. package/dist/jobs/post-record.js +21 -0
  177. package/dist/jobs/post-to-x.js +21 -0
  178. package/dist/jobs/prepare-search-reference.js +190 -0
  179. package/dist/jobs/prepare-temporal-search.js +16 -0
  180. package/dist/jobs/process-audio.js +67 -0
  181. package/dist/jobs/process-image.js +58 -0
  182. package/dist/jobs/process-upload.js +166 -0
  183. package/dist/jobs/process-video.js +438 -0
  184. package/dist/jobs/register-all.js +161 -0
  185. package/dist/jobs/relayout-notebook.js +78 -0
  186. package/dist/jobs/remove-sound.js +299 -0
  187. package/dist/jobs/render-project.js +380 -0
  188. package/dist/jobs/render-qr-outro.js +138 -0
  189. package/dist/jobs/render-timeline.js +283 -0
  190. package/dist/jobs/repair-search-indexes.js +42 -0
  191. package/dist/jobs/replace-video-audio.js +64 -0
  192. package/dist/jobs/restore-archive-clip.js +53 -0
  193. package/dist/jobs/reverse-video.js +53 -0
  194. package/dist/jobs/rewrite-script.js +173 -0
  195. package/dist/jobs/run-notebook.js +3335 -0
  196. package/dist/jobs/semantic-index.js +797 -0
  197. package/dist/jobs/smooth-scene-concat.js +257 -0
  198. package/dist/jobs/splice-video.js +280 -0
  199. package/dist/jobs/split-video.js +127 -0
  200. package/dist/jobs/suggest-cuts.js +149 -0
  201. package/dist/jobs/suggest-sfx.js +135 -0
  202. package/dist/jobs/sync-video-to-audio.js +329 -0
  203. package/dist/jobs/tag-asset.js +266 -0
  204. package/dist/jobs/task-failure.js +16 -0
  205. package/dist/jobs/timewarp-video.js +152 -0
  206. package/dist/jobs/transcribe-video-audio.js +117 -0
  207. package/dist/jobs/transcribe.js +35 -0
  208. package/dist/jobs/translate-transcript.js +55 -0
  209. package/dist/jobs/trim-audio.js +133 -0
  210. package/dist/jobs/trim-video.js +291 -0
  211. package/dist/jobs/xai-paygate.js +26 -0
  212. package/dist/keyword-placement-cache.js +74 -0
  213. package/dist/lazy-render.js +142 -0
  214. package/dist/library-browse.js +78 -0
  215. package/dist/lightning-defaults.js +206 -0
  216. package/dist/load-env.js +12 -0
  217. package/dist/local-search-models.js +206 -0
  218. package/dist/local-upload.js +318 -0
  219. package/dist/logger.js +69 -0
  220. package/dist/media/adapters/alibaba.js +20 -0
  221. package/dist/media/adapters/elevenlabs.js +58 -0
  222. package/dist/media/adapters/fal.js +45 -0
  223. package/dist/media/adapters/index.js +26 -0
  224. package/dist/media/adapters/kie.js +120 -0
  225. package/dist/media/adapters/pending.js +21 -0
  226. package/dist/media/adapters/types.js +5 -0
  227. package/dist/media/adapters/xai.js +18 -0
  228. package/dist/media/catalog.js +236 -0
  229. package/dist/media/complete.js +191 -0
  230. package/dist/media/cost.js +45 -0
  231. package/dist/media/descriptor.js +157 -0
  232. package/dist/media/endpoints.js +11 -0
  233. package/dist/media/frame-options.js +18 -0
  234. package/dist/media/insert-video-timing.js +17 -0
  235. package/dist/media/json-schema.js +75 -0
  236. package/dist/media/legacy-task-types.js +129 -0
  237. package/dist/media/listing.js +24 -0
  238. package/dist/media/model-id.js +14 -0
  239. package/dist/media/preview.js +86 -0
  240. package/dist/media/prompt-dialects.js +65 -0
  241. package/dist/media/ref-fields.js +106 -0
  242. package/dist/media/refs.js +131 -0
  243. package/dist/media/request.js +188 -0
  244. package/dist/media/types.js +6 -0
  245. package/dist/media/wire.js +161 -0
  246. package/dist/media-library.js +535 -0
  247. package/dist/media-output-path.js +33 -0
  248. package/dist/multyculty-workflow.js +757 -0
  249. package/dist/notebook-action-catalog.js +608 -0
  250. package/dist/notebook-cell-outputs.js +726 -0
  251. package/dist/notebook-credit-retry.js +45 -0
  252. package/dist/notebook-fixtures.js +8 -0
  253. package/dist/notebook-generation-contract.js +77 -0
  254. package/dist/notebook-generation-controls.js +145 -0
  255. package/dist/notebook-goal-layout.js +40 -0
  256. package/dist/notebook-graph.js +37 -0
  257. package/dist/notebook-image-edit.js +84 -0
  258. package/dist/notebook-initial-generation-prompt.js +222 -0
  259. package/dist/notebook-live-analysis.js +941 -0
  260. package/dist/notebook-media-ops.js +579 -0
  261. package/dist/notebook-mutations.js +20 -0
  262. package/dist/notebook-placement.js +41 -0
  263. package/dist/notebook-provider-failure.js +87 -0
  264. package/dist/notebook-published-post.js +26 -0
  265. package/dist/notebook-reference-resolver.js +227 -0
  266. package/dist/notebook-relayout.js +270 -0
  267. package/dist/notebook-run-plan.js +519 -0
  268. package/dist/notebook-shots.js +162 -0
  269. package/dist/notebook-workflow.js +856 -0
  270. package/dist/one-password.js +200 -0
  271. package/dist/openai-intelligence.js +99 -0
  272. package/dist/original-media-file.js +12 -0
  273. package/dist/osg-assets.js +36 -0
  274. package/dist/osg-placement-cache.js +64 -0
  275. package/dist/osg-settings.js +27 -0
  276. package/dist/paid-tools.js +22 -0
  277. package/dist/platform-guidance.js +26 -0
  278. package/dist/posts/dropbox-auth.js +91 -0
  279. package/dist/posts/dropbox-upload.js +132 -0
  280. package/dist/posts/icloud-drive.js +134 -0
  281. package/dist/posts/icloud-status.js +64 -0
  282. package/dist/posts/post-contract.js +2 -0
  283. package/dist/posts/post-copy-format.js +43 -0
  284. package/dist/posts/post-copy.js +193 -0
  285. package/dist/posts/post-folder.js +18 -0
  286. package/dist/posts/post-metrics.js +119 -0
  287. package/dist/posts/post-store.js +340 -0
  288. package/dist/posts/x-post-target.js +43 -0
  289. package/dist/preview-payload.js +90 -0
  290. package/dist/prompt-surfaces.js +22 -0
  291. package/dist/provider-prompt-transform.js +77 -0
  292. package/dist/provider-status-link.js +21 -0
  293. package/dist/provider-task-record.js +78 -0
  294. package/dist/provider-task-status.js +47 -0
  295. package/dist/provider-usage.js +294 -0
  296. package/dist/queue.js +810 -0
  297. package/dist/reference-video-duration.js +17 -0
  298. package/dist/remotion/BlurFillVideo.js +51 -0
  299. package/dist/remotion/KeywordGraphicsOverlay.js +68 -0
  300. package/dist/remotion/OsgOverlay.js +30 -0
  301. package/dist/remotion/QrOutroComposition.js +47 -0
  302. package/dist/remotion/RenderComposition.js +11 -0
  303. package/dist/remotion/browser.js +59 -0
  304. package/dist/remotion/constants.js +35 -0
  305. package/dist/remotion/index.js +66 -0
  306. package/dist/remotion/keyword-graphics.js +151 -0
  307. package/dist/remotion/keyword-placement.js +182 -0
  308. package/dist/remotion/osg-spec.js +198 -0
  309. package/dist/remotion/public/qr-outro-impact.wav +0 -0
  310. package/dist/remotion/qr-outro-audio.js +1 -0
  311. package/dist/remotion/qr-outro-spec.js +211 -0
  312. package/dist/remotion/shared-bundle.js +37 -0
  313. package/dist/remotion/subtitles/AliveCaptions.js +83 -0
  314. package/dist/remotion/subtitles/Highlight.js +58 -0
  315. package/dist/remotion/subtitles/Impact.js +67 -0
  316. package/dist/remotion/subtitles/SubtitleOverlay.js +9 -0
  317. package/dist/remotion/subtitles/alive-layout.js +321 -0
  318. package/dist/remotion/subtitles/alive-types.js +17 -0
  319. package/dist/remotion/subtitles/chunker.js +68 -0
  320. package/dist/remotion/subtitles/segments.js +42 -0
  321. package/dist/remotion/subtitles/styles.js +4 -0
  322. package/dist/remotion/subtitles/types.js +1 -0
  323. package/dist/remotion/util.js +18 -0
  324. package/dist/remotion-cancellation.js +15 -0
  325. package/dist/rename-helpers.js +204 -0
  326. package/dist/rhythm/analyze.js +128 -0
  327. package/dist/rhythm/contract.js +14 -0
  328. package/dist/rhythm/draft-model.js +19 -0
  329. package/dist/rhythm/energy.js +133 -0
  330. package/dist/rhythm/fft.js +44 -0
  331. package/dist/rhythm/model.js +71 -0
  332. package/dist/rhythm/music-analysis.js +124 -0
  333. package/dist/rhythm/music-draft.js +261 -0
  334. package/dist/rhythm/onset.js +101 -0
  335. package/dist/rhythm/run-worker.js +51 -0
  336. package/dist/rhythm/score.js +47 -0
  337. package/dist/rhythm/section-labels.js +168 -0
  338. package/dist/rhythm/sequence-rhythm.js +67 -0
  339. package/dist/rhythm/synthesize.js +103 -0
  340. package/dist/rhythm/timing.js +98 -0
  341. package/dist/rhythm/track.js +320 -0
  342. package/dist/rhythm/visual-fit.js +4 -0
  343. package/dist/rhythm/worker.js +25 -0
  344. package/dist/scene-detect.js +200 -0
  345. package/dist/scene-transition.js +12 -0
  346. package/dist/scheduled-posts.js +200 -0
  347. package/dist/search-index-coverage.js +13 -0
  348. package/dist/search-index-media.js +71 -0
  349. package/dist/search-reference-store.js +60 -0
  350. package/dist/search-scoring.js +17 -0
  351. package/dist/serve.js +125 -0
  352. package/dist/server-lock.js +143 -0
  353. package/dist/server.js +42 -0
  354. package/dist/srt.js +16 -0
  355. package/dist/tag-backfill-state.js +121 -0
  356. package/dist/tag-settings.js +30 -0
  357. package/dist/tag-vocabulary.js +237 -0
  358. package/dist/test-queue-cleanup.js +1 -0
  359. package/dist/time-parse.js +50 -0
  360. package/dist/timeline-helpers.js +554 -0
  361. package/dist/timewarp.js +87 -0
  362. package/dist/tools/_image-inputs.js +9 -0
  363. package/dist/tools/_media-refs.js +84 -0
  364. package/dist/tools/_shared.js +59 -0
  365. package/dist/tools/_suggest-shared.js +279 -0
  366. package/dist/tools/add-timeline-audio.js +73 -0
  367. package/dist/tools/alive-captions.js +157 -0
  368. package/dist/tools/analyze-jokes.js +56 -0
  369. package/dist/tools/analyze-video.js +66 -0
  370. package/dist/tools/animate-keywords.js +217 -0
  371. package/dist/tools/api-status.js +42 -0
  372. package/dist/tools/application-commands.js +34 -0
  373. package/dist/tools/apply-cuts.js +112 -0
  374. package/dist/tools/apply-sfx.js +152 -0
  375. package/dist/tools/apply-watermark.js +670 -0
  376. package/dist/tools/archive-clip.js +74 -0
  377. package/dist/tools/asset-history.js +61 -0
  378. package/dist/tools/asset-manifest.js +24 -0
  379. package/dist/tools/asset-tags.js +151 -0
  380. package/dist/tools/assign-slot.js +97 -0
  381. package/dist/tools/audio-boost.js +107 -0
  382. package/dist/tools/audio-mix.js +121 -0
  383. package/dist/tools/book-usage.js +16 -0
  384. package/dist/tools/chain.js +136 -0
  385. package/dist/tools/change-video-speed.js +105 -0
  386. package/dist/tools/chat-history.js +5 -0
  387. package/dist/tools/chat.js +848 -0
  388. package/dist/tools/clean-video-audio.js +97 -0
  389. package/dist/tools/clear-failed-tasks.js +66 -0
  390. package/dist/tools/cold-open.js +114 -0
  391. package/dist/tools/command-plan.js +1680 -0
  392. package/dist/tools/commands.js +1666 -0
  393. package/dist/tools/concat-images.js +236 -0
  394. package/dist/tools/connect-service.js +55 -0
  395. package/dist/tools/copy-file-to-clipboard.js +58 -0
  396. package/dist/tools/create-character.js +114 -0
  397. package/dist/tools/create-qart.js +253 -0
  398. package/dist/tools/create-qr-outro.js +149 -0
  399. package/dist/tools/create-qrbtf.js +215 -0
  400. package/dist/tools/create-watermark.js +97 -0
  401. package/dist/tools/crop-asset.js +18 -0
  402. package/dist/tools/crop-image.js +120 -0
  403. package/dist/tools/crop-video.js +97 -0
  404. package/dist/tools/delete-asset.js +104 -0
  405. package/dist/tools/describe-image.js +69 -0
  406. package/dist/tools/detect-scenes.js +44 -0
  407. package/dist/tools/doctor.js +126 -0
  408. package/dist/tools/download-video.js +114 -0
  409. package/dist/tools/duplicate-asset.js +184 -0
  410. package/dist/tools/edit-timeline.js +59 -0
  411. package/dist/tools/editing-moments.js +105 -0
  412. package/dist/tools/estimate-cost.js +171 -0
  413. package/dist/tools/even-split.js +295 -0
  414. package/dist/tools/export-asset-file.js +22 -0
  415. package/dist/tools/export-asset.js +140 -0
  416. package/dist/tools/export-sequence.js +250 -0
  417. package/dist/tools/export-to-downloads.js +54 -0
  418. package/dist/tools/export-transcript-srt.js +79 -0
  419. package/dist/tools/extend-audio.js +100 -0
  420. package/dist/tools/extract-audio.js +174 -0
  421. package/dist/tools/extract-frame.js +108 -0
  422. package/dist/tools/extract-notebook-audio.js +173 -0
  423. package/dist/tools/extract-notebook-frame.js +160 -0
  424. package/dist/tools/extract-video-clip.js +101 -0
  425. package/dist/tools/generate-media.js +157 -0
  426. package/dist/tools/generation-payload.js +48 -0
  427. package/dist/tools/get-timeline.js +12 -0
  428. package/dist/tools/get-watermark-config.js +38 -0
  429. package/dist/tools/goal.js +644 -0
  430. package/dist/tools/image-subjects.js +711 -0
  431. package/dist/tools/index.js +323 -0
  432. package/dist/tools/inject-first-frame.js +154 -0
  433. package/dist/tools/invert-image.js +70 -0
  434. package/dist/tools/isolate-vocals.js +66 -0
  435. package/dist/tools/list-assets.js +98 -0
  436. package/dist/tools/list-cell-generations.js +225 -0
  437. package/dist/tools/list-elevenlabs-voices.js +163 -0
  438. package/dist/tools/log-wrapper.js +29 -0
  439. package/dist/tools/make-cover-song.js +95 -0
  440. package/dist/tools/mark-asset-seen.js +23 -0
  441. package/dist/tools/media-library.js +245 -0
  442. package/dist/tools/move-timeline-audio.js +35 -0
  443. package/dist/tools/music-analysis.js +60 -0
  444. package/dist/tools/music-draft.js +29 -0
  445. package/dist/tools/pad-video.js +87 -0
  446. package/dist/tools/post-record.js +83 -0
  447. package/dist/tools/post.js +168 -0
  448. package/dist/tools/prompt-history.js +164 -0
  449. package/dist/tools/provider-key-status.js +248 -0
  450. package/dist/tools/qrbtf-render.js +849 -0
  451. package/dist/tools/read-asset-metadata.js +26 -0
  452. package/dist/tools/read-file-at-commit.js +29 -0
  453. package/dist/tools/read-transcript.js +41 -0
  454. package/dist/tools/reconcile-project-work.js +1452 -0
  455. package/dist/tools/recover-asset.js +28 -0
  456. package/dist/tools/remove-sound.js +72 -0
  457. package/dist/tools/remove-timeline-audio.js +34 -0
  458. package/dist/tools/remove-timeline-slot.js +40 -0
  459. package/dist/tools/rename-all.js +434 -0
  460. package/dist/tools/rename-asset.js +34 -0
  461. package/dist/tools/render-settings.js +47 -0
  462. package/dist/tools/render-timeline.js +120 -0
  463. package/dist/tools/render.js +384 -0
  464. package/dist/tools/reorder-character-images.js +45 -0
  465. package/dist/tools/replace-video-audio.js +133 -0
  466. package/dist/tools/restore-archive-clips.js +209 -0
  467. package/dist/tools/restore-asset-version.js +37 -0
  468. package/dist/tools/retry-generation.js +54 -0
  469. package/dist/tools/reverse-video.js +74 -0
  470. package/dist/tools/rewrite-prompt.js +388 -0
  471. package/dist/tools/rewrite-script.js +67 -0
  472. package/dist/tools/schema-from-apicity.js +66 -0
  473. package/dist/tools/search-archive.js +150 -0
  474. package/dist/tools/semantic-search.js +1214 -0
  475. package/dist/tools/sequence-rhythm.js +43 -0
  476. package/dist/tools/set-character-metadata.js +84 -0
  477. package/dist/tools/set-env-secret.js +40 -0
  478. package/dist/tools/set-osg.js +143 -0
  479. package/dist/tools/show-folder.js +37 -0
  480. package/dist/tools/smooth-scene-concat.js +122 -0
  481. package/dist/tools/splice-video.js +149 -0
  482. package/dist/tools/split-video.js +202 -0
  483. package/dist/tools/suggest-cuts.js +45 -0
  484. package/dist/tools/suggest-sfx.js +43 -0
  485. package/dist/tools/sync-video-to-audio.js +115 -0
  486. package/dist/tools/tag-backfill.js +177 -0
  487. package/dist/tools/timewarp-video.js +85 -0
  488. package/dist/tools/tool-def.js +1 -0
  489. package/dist/tools/tool-json-schema.js +116 -0
  490. package/dist/tools/transcribe.js +90 -0
  491. package/dist/tools/translate-subtitles.js +59 -0
  492. package/dist/tools/trim-audio.js +139 -0
  493. package/dist/tools/trim-video.js +168 -0
  494. package/dist/tools/update-script.js +25 -0
  495. package/dist/tools/update-timeline-audio.js +40 -0
  496. package/dist/tools/upload-file.js +41 -0
  497. package/dist/tools/verify-x-mentions.js +141 -0
  498. package/dist/tools/video-editing-command-contract.js +291 -0
  499. package/dist/tools/video-source-reference.js +30 -0
  500. package/dist/tools/video-time-input.js +18 -0
  501. package/dist/tools/videobook-domain.js +2021 -0
  502. package/dist/tools/wait-for-asset-ready.js +143 -0
  503. package/dist/tools/workspace-actions.js +54 -0
  504. package/dist/transcript-projection.js +202 -0
  505. package/dist/transcript-srt.js +33 -0
  506. package/dist/upload.js +76 -0
  507. package/dist/usage-providers.js +25 -0
  508. package/dist/video-analysis-projection.js +46 -0
  509. package/dist/video-poster.js +33 -0
  510. package/dist/video-proxy.js +99 -0
  511. package/dist/vision-regions.js +111 -0
  512. package/dist/voice-actions.js +90 -0
  513. package/dist/voice-audio.js +55 -0
  514. package/dist/voice-config.js +122 -0
  515. package/dist/voice-continuation.js +54 -0
  516. package/dist/voice-contract.js +1 -0
  517. package/dist/voice-http.js +145 -0
  518. package/dist/voice-reply-progress.js +55 -0
  519. package/dist/voice-session.js +992 -0
  520. package/dist/watermark-badge.js +259 -0
  521. package/dist/watermark-config.js +111 -0
  522. package/dist/watermark-handle-position.js +55 -0
  523. package/dist/watermark-platform.js +15 -0
  524. package/dist/watermark-timing.js +16 -0
  525. package/dist/workspace-action-catalog.js +315 -0
  526. package/dist/workspace-action-contract.js +21 -0
  527. package/dist/workspace-action-groups.js +98 -0
  528. package/dist/workspace-action-surfaces.js +7 -0
  529. package/dist/x/account.js +88 -0
  530. package/dist/x/authenticated-handle.js +30 -0
  531. package/dist/x/oauth-flow.js +191 -0
  532. package/dist/x/post-contract.js +1 -0
  533. package/dist/x/post-metadata.js +44 -0
  534. package/dist/x/post-to-x.js +456 -0
  535. package/dist/x/published-post-cell.js +90 -0
  536. package/dist/x/x-tokens.js +69 -0
  537. package/dist/ytdlp-binary.js +65 -0
  538. package/dist/zod-slots.js +334 -0
  539. package/package.json +84 -4
  540. package/ui/assets/alive-captions-BWb5nLKj.css +1 -0
  541. package/ui/assets/alive-captions-DPEr1nKM.js +1 -0
  542. package/ui/assets/confirm-bodies-D8ouHXKy.js +1 -0
  543. package/ui/assets/confirm-bodies-DrMhaXC6.css +1 -0
  544. package/ui/assets/fullscreen-B7nw4YbE.js +1 -0
  545. package/ui/assets/goal-ui-DK-xTCK0.js +2 -0
  546. package/ui/assets/goal-ui-DXdUM2KF.css +1 -0
  547. package/ui/assets/index-DzPAnCUj.css +1 -0
  548. package/ui/assets/index-kS1905s5.js +37 -0
  549. package/ui/assets/inspector-MlzQbTVo.js +1 -0
  550. package/ui/assets/jobs-ui-CoinOWx0.js +1 -0
  551. package/ui/assets/music-video-Cbe80kmQ.js +1 -0
  552. package/ui/assets/music-video-DPaiKKEq.css +1 -0
  553. package/ui/assets/ui-Oirr9VOf.css +1 -0
  554. package/ui/assets/ui-xo_Y1yQg.js +2 -0
  555. package/ui/assets/vendor-D2rfiZ5j.css +1 -0
  556. package/ui/assets/vendor-DfTCWgKm.js +19 -0
  557. package/ui/assets/vendor-flow-BXbeWxW7.js +7 -0
  558. package/ui/assets/vendor-flow-BnuhLJ6X.css +1 -0
  559. package/ui/assets/vendor-icons-CGqhCIXV.js +1 -0
  560. package/ui/assets/vendor-mcp-BTlEJcHV.js +67 -0
  561. package/ui/assets/vendor-react-Dj7D1fD4.js +9 -0
  562. package/ui/assets/vendor-remotion-CH1HXrlc.js +95 -0
  563. package/ui/assets/videobook-Qlp9HQFu.js +20 -0
  564. package/ui/favicon.svg +8 -0
  565. package/ui/index.html +126 -0
@@ -0,0 +1,1680 @@
1
+ import { createHash } from "node:crypto";
2
+ import { findAudioFile, findPrimaryMediaFile, MVP_CONTRACT_VERSION, sequenceFramesToSourceTicks, sourceTicksToSequenceFrames, } from "videobook-engine";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import { z } from "zod";
5
+ import { scanNotebookMentions } from "videobook-engine/notebook-mentions";
6
+ import { fittedTransform } from "../rhythm/visual-fit.js";
7
+ import { resolveArtifactRefs } from "../artifact-refs.js";
8
+ const bookSchema = { book_id: z.string().uuid() };
9
+ export function sequenceContinuationRevision(sequence) {
10
+ const { revision: _revision, ...content } = sequence;
11
+ return createHash("sha256").update(JSON.stringify(content)).digest("hex");
12
+ }
13
+ const intentSchema = z.record(z.string(), z.unknown());
14
+ const editRequestSchema = z.discriminatedUnion("kind", [
15
+ z.object({
16
+ kind: z.literal("append"),
17
+ artifact_id: z.string().uuid(),
18
+ track_id: z.string().uuid(),
19
+ }),
20
+ z.object({
21
+ kind: z.literal("remove_clip"),
22
+ clip_id: z.string().uuid(),
23
+ ripple: z.boolean(),
24
+ }),
25
+ z.object({
26
+ kind: z.literal("insert"),
27
+ artifact_id: z.string().uuid(),
28
+ track_id: z.string().uuid().optional(),
29
+ timeline_start_frame: z.number().int().nonnegative(),
30
+ duration_frames: z.number().int().positive().optional(),
31
+ source_start_tick: z.number().int().nonnegative().optional(),
32
+ source_duration_ticks: z.number().int().positive().optional(),
33
+ mode: z.enum(["insert", "overwrite"]).optional(),
34
+ }),
35
+ z.object({
36
+ kind: z.literal("trim"),
37
+ clip_id: z.string().uuid(),
38
+ timeline_start_frame: z.number().int().nonnegative(),
39
+ duration_frames: z.number().int().positive(),
40
+ edge: z.enum(["start", "end"]).optional(),
41
+ ripple: z.boolean().optional(),
42
+ timeline_limit_frame: z.number().int().positive().optional(),
43
+ }),
44
+ z.object({
45
+ kind: z.literal("volume"),
46
+ clip_id: z.string().uuid(),
47
+ gain_db: z.number().min(-96).max(24),
48
+ muted: z.boolean().optional(),
49
+ fade_in_frames: z.number().int().nonnegative().optional(),
50
+ fade_out_frames: z.number().int().nonnegative().optional(),
51
+ }),
52
+ z.object({
53
+ kind: z.literal("duck"),
54
+ narration_clip_id: z.string().uuid(),
55
+ reduction_db: z.number().min(-36).max(-1),
56
+ }),
57
+ z.object({
58
+ kind: z.literal("move"),
59
+ clip_id: z.string().uuid(),
60
+ track_id: z.string().uuid(),
61
+ timeline_start_frame: z.number().int().nonnegative(),
62
+ }),
63
+ z.object({
64
+ kind: z.literal("split"),
65
+ clip_id: z.string().uuid(),
66
+ split_frame: z.number().int().nonnegative(),
67
+ }),
68
+ z.object({
69
+ kind: z.literal("delete"),
70
+ clip_id: z.string().uuid(),
71
+ ripple: z.boolean(),
72
+ }),
73
+ z.object({
74
+ kind: z.literal("gap"),
75
+ timeline_start_frame: z.number().int().nonnegative(),
76
+ duration_frames: z.number().int().positive(),
77
+ track_ids: z.array(z.string().uuid()).min(1).optional(),
78
+ }),
79
+ z.object({
80
+ kind: z.literal("transform"),
81
+ clip_id: z.string().uuid(),
82
+ fit: z.enum(["fit", "fill", "crop"]).optional(),
83
+ position_x: z.number().optional(),
84
+ position_y: z.number().optional(),
85
+ scale_x: z.number().positive().optional(),
86
+ scale_y: z.number().positive().optional(),
87
+ rotation_degrees: z.number().optional(),
88
+ opacity: z.number().min(0).max(1).optional(),
89
+ crop_top: z.number().min(0).max(1).optional(),
90
+ crop_right: z.number().min(0).max(1).optional(),
91
+ crop_bottom: z.number().min(0).max(1).optional(),
92
+ crop_left: z.number().min(0).max(1).optional(),
93
+ }),
94
+ z.object({
95
+ kind: z.literal("reframe"),
96
+ clip_id: z.string().uuid(),
97
+ }),
98
+ z.object({
99
+ kind: z.literal("speed"),
100
+ clip_id: z.string().uuid(),
101
+ numerator: z.number().int().positive(),
102
+ denominator: z.number().int().positive(),
103
+ reverse: z.boolean(),
104
+ audio_policy: z.enum(["preserve-pitch", "resample", "mute"]),
105
+ ripple: z.boolean().optional(),
106
+ timeline_limit_frame: z.number().int().positive().optional(),
107
+ }),
108
+ z.object({
109
+ kind: z.literal("transition"),
110
+ outgoing_clip_id: z.string().uuid(),
111
+ incoming_clip_id: z.string().uuid(),
112
+ transition_kind: z.enum(["cut", "dissolve"]).nullable(),
113
+ duration_frames: z.number().int().positive().optional(),
114
+ alignment: z.enum(["start", "center", "end"]).optional(),
115
+ }),
116
+ z.object({
117
+ kind: z.literal("caption"),
118
+ cue_id: z.string().uuid().optional(),
119
+ track_id: z.string().uuid().optional(),
120
+ timeline_start_frame: z.number().int().nonnegative(),
121
+ duration_frames: z.number().int().positive(),
122
+ text: z.string().min(1),
123
+ style_id: z.enum(["clean", "bold", "high-contrast"]),
124
+ speaker: z.string().min(1).optional(),
125
+ }),
126
+ z.object({
127
+ kind: z.literal("music_video_setup"),
128
+ audio_artifact_id: z.string().uuid(),
129
+ audio_track_id: z.string().uuid(),
130
+ }),
131
+ z.object({
132
+ kind: z.literal("append_clips"),
133
+ track_id: z.string().uuid(),
134
+ artifact_ids: z.array(z.string().uuid()).min(1).max(20),
135
+ timeline_limit_frame: z.number().int().positive(),
136
+ fit: z.enum(["source", "fill"]).optional(),
137
+ visual_fit: z.enum(["fit", "fill"]).optional(),
138
+ }),
139
+ z.object({
140
+ kind: z.literal("reorder_clips"),
141
+ track_id: z.string().uuid(),
142
+ clip_ids: z.array(z.string().uuid()).min(1).max(500),
143
+ timeline_limit_frame: z.number().int().positive(),
144
+ }),
145
+ ]);
146
+ const generationPlanSchema = z.object({
147
+ tool: z.string().regex(/^(?:generate|extend|upscale)_/u),
148
+ input: z.record(z.string(), z.unknown()),
149
+ provider: z.string().min(1),
150
+ model: z.string().min(1),
151
+ estimated_cost_usd: z.number().nonnegative().optional(),
152
+ estimate_unavailable: z.boolean().optional(),
153
+ track_id: z.string().uuid(),
154
+ timeline_start_frame: z.number().int().nonnegative(),
155
+ duration_frames: z.number().int().positive().optional(),
156
+ });
157
+ const UNSUPPORTED_CHAT_EDIT_MESSAGE = "I can safely plan insert, trim, volume, ducking, captions, move, split, delete, gaps, transforms, speed, and transitions. Name the action and its target.";
158
+ function json(value) {
159
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
160
+ }
161
+ function failure(message) {
162
+ return { isError: true, content: [{ type: "text", text: message }] };
163
+ }
164
+ function requiredTrack(sequence, trackId, kind) {
165
+ const track = trackId
166
+ ? sequence.tracks.find((candidate) => candidate.trackId === trackId)
167
+ : sequence.tracks.find((candidate) => candidate.kind === kind && candidate.enabled && !candidate.locked);
168
+ if (!track || track.kind !== kind || !track.enabled || track.locked) {
169
+ throw new Error(`No editable ${kind} track is available`);
170
+ }
171
+ return track;
172
+ }
173
+ function clipEnd(clip) {
174
+ return clip.timelineStartFrame + clip.durationFrames;
175
+ }
176
+ function isTimedClip(clip) {
177
+ return clip.source.kind === "timed";
178
+ }
179
+ function clipsOnTrack(sequence, trackId) {
180
+ return sequence.clips
181
+ .filter((clip) => clip.trackId === trackId)
182
+ .sort((left, right) => left.timelineStartFrame - right.timelineStartFrame
183
+ || left.clipId.localeCompare(right.clipId));
184
+ }
185
+ function timelineFramesForSource(ticks, timeBase, frameRate, speed) {
186
+ const frames = ticks
187
+ * timeBase.numerator
188
+ * frameRate.numerator
189
+ * speed.denominator
190
+ / timeBase.denominator
191
+ / frameRate.denominator
192
+ / speed.numerator;
193
+ return Math.max(1, Math.ceil(frames));
194
+ }
195
+ function sourceTicksForTimeline(frames, sequence, clip) {
196
+ return sequenceFramesToSourceTicks(frames * clip.speed.numerator, sequence.frameRate, {
197
+ numerator: clip.source.range.timeBase.numerator * clip.speed.denominator,
198
+ denominator: clip.source.range.timeBase.denominator,
199
+ }, "nearest");
200
+ }
201
+ function shiftedTrackEnd(sequence, trackId, selectedClipId, selectedStart, selectedDuration, shiftFromFrame, deltaFrames) {
202
+ return Math.max(selectedStart + selectedDuration, ...clipsOnTrack(sequence, trackId)
203
+ .filter((clip) => clip.clipId !== selectedClipId)
204
+ .map((clip) => clipEnd(clip) + (clip.timelineStartFrame >= shiftFromFrame ? deltaFrames : 0)));
205
+ }
206
+ function requireTimelineLimit(limit, endFrame) {
207
+ if (limit !== undefined && endFrame > limit) {
208
+ throw new Error(`Music-video edit ends at frame ${endFrame}, beyond soundtrack frame ${limit}`);
209
+ }
210
+ }
211
+ function splitFrames(total, count) {
212
+ const base = Math.floor(total / count);
213
+ const extra = total % count;
214
+ return Array.from({ length: count }, (_, index) => base + (index >= count - extra ? 1 : 0));
215
+ }
216
+ function nativeClipFrames(sequence, clip) {
217
+ if (clip.source.kind !== "timed")
218
+ return clip.durationFrames;
219
+ return timelineFramesForSource(clip.source.range.durationTicks, clip.source.range.timeBase, sequence.frameRate, { numerator: 1, denominator: 1 });
220
+ }
221
+ function requiredVideoStream(engine, artifactId) {
222
+ const artifact = engine.artifacts.get(artifactId);
223
+ if (!artifact.ok)
224
+ throw new Error(artifact.error.message);
225
+ if (artifact.value.kind !== "video" && artifact.value.kind !== "final") {
226
+ throw new Error("Music-video sources must be video or final artifacts");
227
+ }
228
+ const streams = engine.streams.list(artifactId);
229
+ if (!streams.ok)
230
+ throw new Error(streams.error.message);
231
+ const stream = streams.value.find((candidate) => candidate.kind === "video");
232
+ if (!stream)
233
+ throw new Error(`Video source has no registered stream: ${artifactId}`);
234
+ return stream;
235
+ }
236
+ function timedVideoPlacement(trackId, timelineStartFrame, durationFrames, artifactId, stream, nativeFrames, visualFit) {
237
+ return {
238
+ kind: "insert-clip",
239
+ mode: "overwrite",
240
+ placement: {
241
+ trackId,
242
+ timelineStartFrame,
243
+ durationFrames,
244
+ source: {
245
+ kind: "timed",
246
+ artifactId,
247
+ range: {
248
+ streamId: stream.streamId,
249
+ objectHash: stream.objectHash,
250
+ startTick: 0,
251
+ durationTicks: stream.durationTicks,
252
+ timeBase: stream.timeBase,
253
+ },
254
+ },
255
+ speed: {
256
+ numerator: Math.max(1, nativeFrames),
257
+ denominator: Math.max(1, durationFrames),
258
+ },
259
+ reverse: false,
260
+ audioPolicy: "mute",
261
+ ...(visualFit ? { transform: fittedTransform(visualFit) } : {}),
262
+ },
263
+ };
264
+ }
265
+ function rebalanceLaneAppend(sequence, trackId, existing, sources, limit) {
266
+ const count = existing.length + sources.length;
267
+ if (limit < count) {
268
+ throw new Error(`Music-video edit ends at frame ${count}, beyond soundtrack frame ${limit}`);
269
+ }
270
+ const slots = splitFrames(limit, count);
271
+ const items = [
272
+ ...existing.map((clip, index) => ({
273
+ kind: "existing",
274
+ clipId: clip.clipId,
275
+ durationFrames: slots[index],
276
+ clip,
277
+ })),
278
+ ...sources.map((source, index) => ({
279
+ kind: "source",
280
+ clipId: uuidv7(),
281
+ durationFrames: slots[existing.length + index],
282
+ source,
283
+ })),
284
+ ];
285
+ const operations = [];
286
+ let temp = limit + 1;
287
+ for (const item of items) {
288
+ if (item.kind === "existing") {
289
+ const clip = item.clip;
290
+ operations.push({
291
+ kind: "move-clip",
292
+ clipId: item.clipId,
293
+ trackId,
294
+ timelineStartFrame: temp,
295
+ });
296
+ operations.push({
297
+ kind: "trim-clip",
298
+ clipId: item.clipId,
299
+ timelineStartFrame: temp,
300
+ durationFrames: item.durationFrames,
301
+ ...(isTimedClip(clip)
302
+ ? { sourceRange: clip.source.range }
303
+ : {}),
304
+ });
305
+ if (isTimedClip(clip)) {
306
+ operations.push({
307
+ kind: "set-clip-speed",
308
+ clipId: item.clipId,
309
+ speed: {
310
+ numerator: Math.max(1, nativeClipFrames(sequence, clip)),
311
+ denominator: Math.max(1, item.durationFrames),
312
+ },
313
+ reverse: clip.reverse,
314
+ audioPolicy: "mute",
315
+ });
316
+ }
317
+ temp += Math.max(clip.durationFrames, item.durationFrames) + 1;
318
+ continue;
319
+ }
320
+ const source = item.source;
321
+ operations.push({
322
+ ...timedVideoPlacement(trackId, temp, item.durationFrames, source.artifactId, source.stream, source.nativeFrames),
323
+ clipId: item.clipId,
324
+ });
325
+ temp += item.durationFrames + 1;
326
+ }
327
+ let start = 0;
328
+ for (const item of items) {
329
+ operations.push({
330
+ kind: "move-clip",
331
+ clipId: item.clipId,
332
+ trackId,
333
+ timelineStartFrame: start,
334
+ });
335
+ start += item.durationFrames;
336
+ }
337
+ requireTimelineLimit(limit, start);
338
+ return operations;
339
+ }
340
+ function rippleMoves(sequence, clip, deltaFrames) {
341
+ if (deltaFrames === 0)
342
+ return [];
343
+ const later = clipsOnTrack(sequence, clip.trackId)
344
+ .filter((candidate) => candidate.clipId !== clip.clipId
345
+ && candidate.timelineStartFrame >= clipEnd(clip))
346
+ .sort((left, right) => deltaFrames > 0
347
+ ? right.timelineStartFrame - left.timelineStartFrame
348
+ : left.timelineStartFrame - right.timelineStartFrame);
349
+ return later.map((candidate) => ({
350
+ kind: "move-clip",
351
+ clipId: candidate.clipId,
352
+ trackId: candidate.trackId,
353
+ timelineStartFrame: candidate.timelineStartFrame + deltaFrames,
354
+ }));
355
+ }
356
+ function clipAudio(clip, muted) {
357
+ return {
358
+ gainDb: clip.audio?.gainDb ?? 0,
359
+ muted,
360
+ fadeInFrames: clip.audio?.fadeInFrames ?? 0,
361
+ fadeOutFrames: clip.audio?.fadeOutFrames ?? 0,
362
+ };
363
+ }
364
+ function audioSetupOperations(engine, sequence, audioArtifactId, audioTrackId) {
365
+ const artifact = engine.artifacts.get(audioArtifactId);
366
+ if (!artifact.ok)
367
+ throw new Error(artifact.error.message);
368
+ if (artifact.value.kind !== "audio") {
369
+ throw new Error("Music-video setup requires an audio artifact");
370
+ }
371
+ const track = requiredTrack(sequence, audioTrackId, "audio");
372
+ if (track.muted)
373
+ throw new Error("The soundtrack track is muted");
374
+ if (sequence.tracks.some((candidate) => candidate.kind === "audio"
375
+ && candidate.trackId !== track.trackId
376
+ && candidate.solo)) {
377
+ throw new Error("Disable solo on other audio tracks before applying the soundtrack");
378
+ }
379
+ const streams = engine.streams.list(audioArtifactId);
380
+ if (!streams.ok)
381
+ throw new Error(streams.error.message);
382
+ const stream = streams.value.find((candidate) => candidate.kind === "audio");
383
+ if (!stream)
384
+ throw new Error("The soundtrack has no registered audio stream");
385
+ const durationFrames = sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "ceil");
386
+ const destinationClips = clipsOnTrack(sequence, track.trackId);
387
+ const existingMaster = destinationClips.length === 1
388
+ && destinationClips[0]?.source.kind === "timed"
389
+ && destinationClips[0].source.artifactId === audioArtifactId
390
+ && destinationClips[0].timelineStartFrame === 0
391
+ ? destinationClips[0]
392
+ : null;
393
+ if (destinationClips.length > 0 && !existingMaster) {
394
+ throw new Error("Choose an empty audio track for the master soundtrack");
395
+ }
396
+ const operations = existingMaster
397
+ ? [
398
+ {
399
+ kind: "trim-clip",
400
+ clipId: existingMaster.clipId,
401
+ timelineStartFrame: 0,
402
+ durationFrames,
403
+ sourceRange: {
404
+ streamId: stream.streamId,
405
+ objectHash: stream.objectHash,
406
+ startTick: 0,
407
+ durationTicks: stream.durationTicks,
408
+ timeBase: stream.timeBase,
409
+ },
410
+ },
411
+ {
412
+ kind: "set-clip-speed",
413
+ clipId: existingMaster.clipId,
414
+ speed: { numerator: 1, denominator: 1 },
415
+ reverse: false,
416
+ audioPolicy: "preserve-pitch",
417
+ },
418
+ {
419
+ kind: "set-clip-audio",
420
+ clipId: existingMaster.clipId,
421
+ audio: clipAudio(existingMaster, false),
422
+ },
423
+ ]
424
+ : [{
425
+ kind: "insert-clip",
426
+ mode: "insert",
427
+ placement: {
428
+ trackId: track.trackId,
429
+ timelineStartFrame: 0,
430
+ durationFrames,
431
+ source: {
432
+ kind: "timed",
433
+ artifactId: audioArtifactId,
434
+ range: {
435
+ streamId: stream.streamId,
436
+ objectHash: stream.objectHash,
437
+ startTick: 0,
438
+ durationTicks: stream.durationTicks,
439
+ timeBase: stream.timeBase,
440
+ },
441
+ },
442
+ speed: { numerator: 1, denominator: 1 },
443
+ reverse: false,
444
+ audioPolicy: "preserve-pitch",
445
+ audio: {
446
+ gainDb: 0,
447
+ muted: false,
448
+ fadeInFrames: 0,
449
+ fadeOutFrames: 0,
450
+ },
451
+ },
452
+ }];
453
+ for (const clip of sequence.clips) {
454
+ if (clip.clipId === existingMaster?.clipId || !clip.enabled)
455
+ continue;
456
+ const clipTrack = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
457
+ if (!clipTrack?.enabled)
458
+ continue;
459
+ if (clipTrack.kind === "audio") {
460
+ if (clipTrack.muted || clip.audio?.muted)
461
+ continue;
462
+ if (clipTrack.locked) {
463
+ throw new Error(`Unlock audio track ${clipTrack.name} before applying the soundtrack`);
464
+ }
465
+ operations.push({
466
+ kind: "set-clip-audio",
467
+ clipId: clip.clipId,
468
+ audio: clipAudio(clip, true),
469
+ });
470
+ continue;
471
+ }
472
+ if (clipTrack.kind === "video"
473
+ && isTimedClip(clip)
474
+ && clip.audioPolicy !== "mute") {
475
+ if (clipTrack.locked) {
476
+ throw new Error(`Unlock video track ${clipTrack.name} before muting embedded audio`);
477
+ }
478
+ operations.push({
479
+ kind: "set-clip-speed",
480
+ clipId: clip.clipId,
481
+ speed: clip.speed,
482
+ reverse: clip.reverse,
483
+ audioPolicy: "mute",
484
+ });
485
+ }
486
+ }
487
+ return operations;
488
+ }
489
+ function rippleTrimOperations(engine, sequence, request, clip) {
490
+ if (!request.edge)
491
+ throw new Error("Ripple trim requires a start or end edge");
492
+ if (isTimedClip(clip) && clip.reverse) {
493
+ throw new Error("Ripple trim is unavailable for reversed clips");
494
+ }
495
+ const oldEnd = clipEnd(clip);
496
+ const newEnd = request.timeline_start_frame + request.duration_frames;
497
+ if (request.edge === "start" && newEnd !== oldEnd) {
498
+ throw new Error("A start-edge trim must preserve the clip end frame");
499
+ }
500
+ if (request.edge === "end"
501
+ && request.timeline_start_frame !== clip.timelineStartFrame) {
502
+ throw new Error("An end-edge trim must preserve the clip start frame");
503
+ }
504
+ let sourceRange = isTimedClip(clip)
505
+ ? { ...clip.source.range }
506
+ : undefined;
507
+ if (isTimedClip(clip)) {
508
+ const stream = engine.streams.get(clip.source.range.streamId);
509
+ if (!stream.ok)
510
+ throw new Error(stream.error.message);
511
+ const durationTicks = sourceTicksForTimeline(request.duration_frames, sequence, clip);
512
+ const sourceEndTick = clip.source.range.startTick
513
+ + clip.source.range.durationTicks;
514
+ const startTick = request.edge === "start"
515
+ ? sourceEndTick - durationTicks
516
+ : clip.source.range.startTick;
517
+ if (startTick < 0
518
+ || durationTicks < 1
519
+ || startTick + durationTicks > stream.value.durationTicks) {
520
+ throw new Error("Trim exceeds the registered source handles");
521
+ }
522
+ sourceRange = {
523
+ ...clip.source.range,
524
+ startTick,
525
+ durationTicks,
526
+ };
527
+ }
528
+ const deltaFrames = newEnd - oldEnd;
529
+ requireTimelineLimit(request.timeline_limit_frame, shiftedTrackEnd(sequence, clip.trackId, clip.clipId, request.timeline_start_frame, request.duration_frames, oldEnd, deltaFrames));
530
+ const trim = {
531
+ kind: "trim-clip",
532
+ clipId: clip.clipId,
533
+ timelineStartFrame: request.timeline_start_frame,
534
+ durationFrames: request.duration_frames,
535
+ ...(sourceRange ? { sourceRange } : {}),
536
+ };
537
+ const moves = rippleMoves(sequence, clip, deltaFrames);
538
+ return deltaFrames > 0 ? [...moves, trim] : [trim, ...moves];
539
+ }
540
+ function rippleSpeedOperations(sequence, request, clip) {
541
+ const durationFrames = timelineFramesForSource(clip.source.range.durationTicks, clip.source.range.timeBase, sequence.frameRate, { numerator: request.numerator, denominator: request.denominator });
542
+ const deltaFrames = durationFrames - clip.durationFrames;
543
+ requireTimelineLimit(request.timeline_limit_frame, shiftedTrackEnd(sequence, clip.trackId, clip.clipId, clip.timelineStartFrame, durationFrames, clipEnd(clip), deltaFrames));
544
+ const trim = {
545
+ kind: "trim-clip",
546
+ clipId: clip.clipId,
547
+ timelineStartFrame: clip.timelineStartFrame,
548
+ durationFrames,
549
+ sourceRange: clip.source.range,
550
+ };
551
+ const speed = {
552
+ kind: "set-clip-speed",
553
+ clipId: clip.clipId,
554
+ speed: {
555
+ numerator: request.numerator,
556
+ denominator: request.denominator,
557
+ },
558
+ reverse: request.reverse,
559
+ audioPolicy: request.audio_policy,
560
+ };
561
+ const moves = rippleMoves(sequence, clip, deltaFrames);
562
+ return deltaFrames > 0
563
+ ? [...moves, trim, speed]
564
+ : [trim, speed, ...moves];
565
+ }
566
+ async function operationsForRequest(engine, sequence, request) {
567
+ if (request.kind === "remove_clip") {
568
+ return [{ kind: "remove-clip", clipId: request.clip_id, ripple: request.ripple }];
569
+ }
570
+ if (request.kind === "append") {
571
+ const track = sequence.tracks.find((entry) => entry.trackId === request.track_id);
572
+ if (!track || track.kind === "caption")
573
+ throw new Error("Choose a video or audio track");
574
+ if (track.locked)
575
+ throw new Error("The destination track is locked");
576
+ const artifact = engine.artifacts.get(request.artifact_id);
577
+ if (!artifact.ok)
578
+ throw new Error(artifact.error.message);
579
+ const compatible = track.kind === "audio"
580
+ ? artifact.value.kind === "audio"
581
+ : artifact.value.kind === "video" || artifact.value.kind === "final";
582
+ if (!compatible)
583
+ throw new Error(`Choose ${track.kind === "audio" ? "an audio" : "a video"} source for this track`);
584
+ const manifest = await engine.files.manifest(request.artifact_id);
585
+ if (!manifest.ok)
586
+ throw new Error(manifest.error.message);
587
+ const file = track.kind === "audio"
588
+ ? findAudioFile(manifest.value.files, artifact.value.kind)
589
+ : findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
590
+ const streams = engine.streams.list(request.artifact_id);
591
+ if (!streams.ok)
592
+ throw new Error(streams.error.message);
593
+ const stream = streams.value.find((entry) => entry.kind === track.kind
594
+ && entry.sourcePath === file?.name && entry.objectHash === file.objectHash);
595
+ if (!stream)
596
+ throw new Error("Wait for the current source media to finish processing before adding it");
597
+ const durationFrames = sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "ceil");
598
+ return [{
599
+ kind: "insert-clip",
600
+ mode: "insert",
601
+ placement: {
602
+ trackId: track.trackId,
603
+ timelineStartFrame: sequence.clips.reduce((end, clip) => clip.trackId === track.trackId ? Math.max(end, clipEnd(clip)) : end, 0),
604
+ durationFrames,
605
+ source: {
606
+ kind: "timed",
607
+ artifactId: request.artifact_id,
608
+ range: {
609
+ streamId: stream.streamId,
610
+ objectHash: stream.objectHash,
611
+ startTick: 0,
612
+ durationTicks: stream.durationTicks,
613
+ timeBase: stream.timeBase,
614
+ },
615
+ },
616
+ speed: { numerator: 1, denominator: 1 },
617
+ reverse: false,
618
+ audioPolicy: "preserve-pitch",
619
+ },
620
+ }];
621
+ }
622
+ if (request.kind === "music_video_setup") {
623
+ return audioSetupOperations(engine, sequence, request.audio_artifact_id, request.audio_track_id);
624
+ }
625
+ if (request.kind === "append_clips") {
626
+ const track = requiredTrack(sequence, request.track_id, "video");
627
+ const existing = clipsOnTrack(sequence, track.trackId);
628
+ const sources = request.artifact_ids.map((artifactId) => {
629
+ const stream = requiredVideoStream(engine, artifactId);
630
+ return {
631
+ artifactId,
632
+ stream,
633
+ nativeFrames: sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "ceil"),
634
+ };
635
+ });
636
+ if ((request.fit ?? "fill") === "source") {
637
+ let timelineStartFrame = Math.max(0, ...existing.map(clipEnd));
638
+ const operations = [];
639
+ for (const source of sources) {
640
+ requireTimelineLimit(request.timeline_limit_frame, timelineStartFrame + source.nativeFrames);
641
+ operations.push(timedVideoPlacement(track.trackId, timelineStartFrame, source.nativeFrames, source.artifactId, source.stream, source.nativeFrames, request.visual_fit));
642
+ timelineStartFrame += source.nativeFrames;
643
+ }
644
+ return operations;
645
+ }
646
+ const laneEnd = Math.max(0, ...existing.map(clipEnd));
647
+ if (laneEnd > request.timeline_limit_frame) {
648
+ requireTimelineLimit(request.timeline_limit_frame, laneEnd);
649
+ }
650
+ return rebalanceLaneAppend(sequence, track.trackId, existing, sources, request.timeline_limit_frame);
651
+ }
652
+ if (request.kind === "reorder_clips") {
653
+ const track = requiredTrack(sequence, request.track_id, "video");
654
+ const current = clipsOnTrack(sequence, track.trackId);
655
+ const currentIds = new Set(current.map((clip) => clip.clipId));
656
+ const requestedIds = new Set(request.clip_ids);
657
+ if (current.length !== request.clip_ids.length
658
+ || currentIds.size !== requestedIds.size
659
+ || request.clip_ids.some((clipId) => !currentIds.has(clipId))) {
660
+ throw new Error("Reorder requires every clip on the main video lane exactly once");
661
+ }
662
+ const byId = new Map(current.map((clip) => [clip.clipId, clip]));
663
+ const gaps = current.map((clip, index) => index === 0
664
+ ? clip.timelineStartFrame
665
+ : clip.timelineStartFrame - clipEnd(current[index - 1]));
666
+ const finalStarts = new Map();
667
+ let cursor = gaps[0] ?? 0;
668
+ request.clip_ids.forEach((clipId, index) => {
669
+ if (index > 0)
670
+ cursor += gaps[index] ?? 0;
671
+ finalStarts.set(clipId, cursor);
672
+ cursor += byId.get(clipId).durationFrames;
673
+ });
674
+ requireTimelineLimit(request.timeline_limit_frame, cursor);
675
+ const unchanged = request.clip_ids.every((clipId, index) => clipId === current[index]?.clipId);
676
+ if (unchanged) {
677
+ return current.map((clip) => ({
678
+ kind: "move-clip",
679
+ clipId: clip.clipId,
680
+ trackId: track.trackId,
681
+ timelineStartFrame: clip.timelineStartFrame,
682
+ }));
683
+ }
684
+ let temporaryCursor = Math.max(request.timeline_limit_frame, ...sequence.clips.map(clipEnd)) + 1;
685
+ const temporary = current.map((clip) => {
686
+ const operation = {
687
+ kind: "move-clip",
688
+ clipId: clip.clipId,
689
+ trackId: track.trackId,
690
+ timelineStartFrame: temporaryCursor,
691
+ };
692
+ temporaryCursor += clip.durationFrames + 1;
693
+ return operation;
694
+ });
695
+ const final = request.clip_ids.map((clipId) => ({
696
+ kind: "move-clip",
697
+ clipId,
698
+ trackId: track.trackId,
699
+ timelineStartFrame: finalStarts.get(clipId),
700
+ }));
701
+ return [...temporary, ...final];
702
+ }
703
+ if (request.kind === "trim") {
704
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
705
+ if (!clip)
706
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
707
+ if (request.ripple) {
708
+ return rippleTrimOperations(engine, sequence, request, clip);
709
+ }
710
+ return [{
711
+ kind: "trim-clip",
712
+ clipId: clip.clipId,
713
+ timelineStartFrame: request.timeline_start_frame,
714
+ durationFrames: request.duration_frames,
715
+ }];
716
+ }
717
+ if (request.kind === "volume") {
718
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
719
+ if (!clip)
720
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
721
+ const track = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
722
+ if (track?.kind !== "audio") {
723
+ throw new Error("Volume changes require a clip on an audio track");
724
+ }
725
+ const current = clip.audio ?? {
726
+ gainDb: 0,
727
+ muted: false,
728
+ fadeInFrames: 0,
729
+ fadeOutFrames: 0,
730
+ };
731
+ return [{
732
+ kind: "set-clip-audio",
733
+ clipId: clip.clipId,
734
+ audio: {
735
+ gainDb: request.gain_db,
736
+ muted: request.muted ?? current.muted,
737
+ fadeInFrames: request.fade_in_frames ?? current.fadeInFrames,
738
+ fadeOutFrames: request.fade_out_frames ?? current.fadeOutFrames,
739
+ },
740
+ }];
741
+ }
742
+ if (request.kind === "duck") {
743
+ const narration = sequence.clips.find((candidate) => candidate.clipId === request.narration_clip_id);
744
+ const narrationTrack = narration && sequence.tracks.find((candidate) => candidate.trackId === narration.trackId);
745
+ if (!narration || narrationTrack?.kind !== "audio") {
746
+ throw new Error("Ducking requires a narration clip on an audio track");
747
+ }
748
+ const narrationEnd = narration.timelineStartFrame + narration.durationFrames;
749
+ const overlaps = sequence.clips.filter((clip) => {
750
+ const track = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
751
+ const clipEnd = clip.timelineStartFrame + clip.durationFrames;
752
+ return clip.clipId !== narration.clipId
753
+ && track?.kind === "audio"
754
+ && track.enabled
755
+ && !track.locked
756
+ && clip.enabled
757
+ && clip.timelineStartFrame < narrationEnd
758
+ && clipEnd > narration.timelineStartFrame;
759
+ });
760
+ if (overlaps.length === 0) {
761
+ throw new Error("No editable audio clips overlap the narration");
762
+ }
763
+ return overlaps.map((clip) => {
764
+ const current = clip.audio ?? {
765
+ gainDb: 0,
766
+ muted: false,
767
+ fadeInFrames: 0,
768
+ fadeOutFrames: 0,
769
+ };
770
+ return {
771
+ kind: "set-clip-audio",
772
+ clipId: clip.clipId,
773
+ audio: {
774
+ ...current,
775
+ gainDb: Math.max(-96, current.gainDb + request.reduction_db),
776
+ },
777
+ };
778
+ });
779
+ }
780
+ if (request.kind === "move") {
781
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
782
+ if (!clip)
783
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
784
+ const target = sequence.tracks.find((candidate) => candidate.trackId === request.track_id);
785
+ const source = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
786
+ if (!target
787
+ || !source
788
+ || target.kind !== source.kind
789
+ || target.locked
790
+ || !target.enabled) {
791
+ throw new Error("Move destination is not a compatible editable track");
792
+ }
793
+ return [{
794
+ kind: "move-clip",
795
+ clipId: clip.clipId,
796
+ trackId: target.trackId,
797
+ timelineStartFrame: request.timeline_start_frame,
798
+ }];
799
+ }
800
+ if (request.kind === "split") {
801
+ if (!sequence.clips.some((candidate) => candidate.clipId === request.clip_id)) {
802
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
803
+ }
804
+ return [{
805
+ kind: "split-clip",
806
+ clipId: request.clip_id,
807
+ splitFrame: request.split_frame,
808
+ }];
809
+ }
810
+ if (request.kind === "delete") {
811
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
812
+ if (!clip)
813
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
814
+ return [{
815
+ kind: "remove-range",
816
+ trackIds: [clip.trackId],
817
+ range: {
818
+ sequenceId: sequence.sequenceId,
819
+ startFrame: clip.timelineStartFrame,
820
+ durationFrames: clip.durationFrames,
821
+ },
822
+ ripple: request.ripple,
823
+ }];
824
+ }
825
+ if (request.kind === "gap") {
826
+ const trackIds = request.track_ids ?? sequence.tracks
827
+ .filter((track) => track.kind !== "caption" && track.enabled && !track.locked)
828
+ .map((track) => track.trackId);
829
+ const selected = new Set(trackIds);
830
+ const clips = sequence.clips
831
+ .filter((clip) => selected.has(clip.trackId)
832
+ && clip.timelineStartFrame >= request.timeline_start_frame)
833
+ .sort((left, right) => right.timelineStartFrame - left.timelineStartFrame);
834
+ if (clips.length === 0)
835
+ throw new Error("No clips follow the requested gap");
836
+ return clips.map((clip) => ({
837
+ kind: "move-clip",
838
+ clipId: clip.clipId,
839
+ trackId: clip.trackId,
840
+ timelineStartFrame: clip.timelineStartFrame + request.duration_frames,
841
+ }));
842
+ }
843
+ if (request.kind === "transform") {
844
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
845
+ if (!clip)
846
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
847
+ const current = clip.transform ?? {
848
+ fit: "fill",
849
+ positionX: 0,
850
+ positionY: 0,
851
+ scaleX: 1,
852
+ scaleY: 1,
853
+ anchorX: 0.5,
854
+ anchorY: 0.5,
855
+ rotationDegrees: 0,
856
+ cropTop: 0,
857
+ cropRight: 0,
858
+ cropBottom: 0,
859
+ cropLeft: 0,
860
+ opacity: 1,
861
+ blendMode: "normal",
862
+ };
863
+ return [{
864
+ kind: "set-clip-transform",
865
+ clipId: clip.clipId,
866
+ transform: {
867
+ ...current,
868
+ fit: request.fit ?? current.fit,
869
+ positionX: request.position_x ?? current.positionX,
870
+ positionY: request.position_y ?? current.positionY,
871
+ scaleX: request.scale_x ?? current.scaleX,
872
+ scaleY: request.scale_y ?? current.scaleY,
873
+ rotationDegrees: request.rotation_degrees ?? current.rotationDegrees,
874
+ opacity: request.opacity ?? current.opacity,
875
+ cropTop: request.crop_top ?? current.cropTop,
876
+ cropRight: request.crop_right ?? current.cropRight,
877
+ cropBottom: request.crop_bottom ?? current.cropBottom,
878
+ cropLeft: request.crop_left ?? current.cropLeft,
879
+ },
880
+ }];
881
+ }
882
+ if (request.kind === "reframe") {
883
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
884
+ if (!clip)
885
+ throw new Error(`Sequence clip not found: ${request.clip_id}`);
886
+ const track = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
887
+ if (track?.kind !== "video") {
888
+ throw new Error("Automatic reframe requires a visual clip");
889
+ }
890
+ const analysis = await engine.files.read(clip.source.artifactId, ".original.analysis.json");
891
+ let subjectPlacement = "center";
892
+ if (analysis.ok) {
893
+ try {
894
+ const parsed = JSON.parse(typeof analysis.value === "string"
895
+ ? analysis.value
896
+ : new TextDecoder().decode(analysis.value));
897
+ if (parsed.subjectPlacement === "left"
898
+ || parsed.subjectPlacement === "right") {
899
+ subjectPlacement = parsed.subjectPlacement;
900
+ }
901
+ }
902
+ catch {
903
+ subjectPlacement = "center";
904
+ }
905
+ }
906
+ const current = clip.transform ?? {
907
+ fit: "fill",
908
+ positionX: 0,
909
+ positionY: 0,
910
+ scaleX: 1,
911
+ scaleY: 1,
912
+ anchorX: 0.5,
913
+ anchorY: 0.5,
914
+ rotationDegrees: 0,
915
+ cropTop: 0,
916
+ cropRight: 0,
917
+ cropBottom: 0,
918
+ cropLeft: 0,
919
+ opacity: 1,
920
+ blendMode: "normal",
921
+ };
922
+ return [{
923
+ kind: "set-clip-transform",
924
+ clipId: clip.clipId,
925
+ transform: {
926
+ ...current,
927
+ fit: "fill",
928
+ positionX: subjectPlacement === "left"
929
+ ? sequence.width * 0.18
930
+ : subjectPlacement === "right"
931
+ ? sequence.width * -0.18
932
+ : 0,
933
+ },
934
+ }];
935
+ }
936
+ if (request.kind === "speed") {
937
+ const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
938
+ if (!clip || !isTimedClip(clip)) {
939
+ throw new Error("Speed and reverse require a timed sequence clip");
940
+ }
941
+ if (request.ripple) {
942
+ return rippleSpeedOperations(sequence, request, clip);
943
+ }
944
+ return [{
945
+ kind: "set-clip-speed",
946
+ clipId: clip.clipId,
947
+ speed: {
948
+ numerator: request.numerator,
949
+ denominator: request.denominator,
950
+ },
951
+ reverse: request.reverse,
952
+ audioPolicy: request.audio_policy,
953
+ }];
954
+ }
955
+ if (request.kind === "transition") {
956
+ const outgoing = sequence.clips.find((candidate) => candidate.clipId === request.outgoing_clip_id);
957
+ const incoming = sequence.clips.find((candidate) => candidate.clipId === request.incoming_clip_id);
958
+ if (!outgoing || !incoming || outgoing.trackId !== incoming.trackId) {
959
+ throw new Error("Transition clips must exist on the same track");
960
+ }
961
+ return [{
962
+ kind: "set-transition",
963
+ outgoingClipId: outgoing.clipId,
964
+ incomingClipId: incoming.clipId,
965
+ transition: request.transition_kind === null
966
+ ? null
967
+ : {
968
+ transitionId: uuidv7(),
969
+ trackId: outgoing.trackId,
970
+ outgoingClipId: outgoing.clipId,
971
+ incomingClipId: incoming.clipId,
972
+ kind: request.transition_kind,
973
+ durationFrames: request.duration_frames ?? 12,
974
+ alignment: request.alignment ?? "center",
975
+ },
976
+ }];
977
+ }
978
+ if (request.kind === "caption") {
979
+ const track = request.track_id
980
+ ? sequence.tracks.find((candidate) => candidate.trackId === request.track_id)
981
+ : sequence.tracks.find((candidate) => candidate.kind === "caption" && candidate.enabled && !candidate.locked);
982
+ if (!track || track.kind !== "caption" || track.locked || !track.enabled) {
983
+ throw new Error("No editable caption track is available");
984
+ }
985
+ return [{
986
+ kind: "upsert-caption-cue",
987
+ cue: {
988
+ cueId: request.cue_id ?? uuidv7(),
989
+ trackId: track.trackId,
990
+ timelineStartFrame: request.timeline_start_frame,
991
+ durationFrames: request.duration_frames,
992
+ text: request.text,
993
+ styleId: request.style_id,
994
+ ...(request.speaker ? { speaker: request.speaker } : {}),
995
+ },
996
+ }];
997
+ }
998
+ const artifact = engine.artifacts.get(request.artifact_id);
999
+ if (!artifact.ok)
1000
+ throw new Error(artifact.error.message);
1001
+ const streams = engine.streams.list(artifact.value.artifactId);
1002
+ if (!streams.ok)
1003
+ throw new Error(streams.error.message);
1004
+ const timedStream = streams.value.find((stream) => stream.kind === "video") ?? streams.value.find((stream) => stream.kind === "audio");
1005
+ if (timedStream) {
1006
+ const track = requiredTrack(sequence, request.track_id, timedStream.kind);
1007
+ const sourceStartTick = request.source_start_tick ?? 0;
1008
+ const sourceDurationTicks = request.source_duration_ticks
1009
+ ?? timedStream.durationTicks - sourceStartTick;
1010
+ if (sourceDurationTicks < 1
1011
+ || sourceStartTick + sourceDurationTicks > timedStream.durationTicks) {
1012
+ throw new Error("Insert source range exceeds the registered stream");
1013
+ }
1014
+ return [{
1015
+ kind: "insert-clip",
1016
+ mode: request.mode ?? "insert",
1017
+ placement: {
1018
+ trackId: track.trackId,
1019
+ timelineStartFrame: request.timeline_start_frame,
1020
+ durationFrames: request.duration_frames ?? Math.max(1, sourceTicksToSequenceFrames(sourceDurationTicks, timedStream.timeBase, sequence.frameRate, "ceil")),
1021
+ source: {
1022
+ kind: "timed",
1023
+ artifactId: artifact.value.artifactId,
1024
+ range: {
1025
+ streamId: timedStream.streamId,
1026
+ objectHash: timedStream.objectHash,
1027
+ startTick: sourceStartTick,
1028
+ durationTicks: sourceDurationTicks,
1029
+ timeBase: timedStream.timeBase,
1030
+ },
1031
+ },
1032
+ speed: { numerator: 1, denominator: 1 },
1033
+ reverse: false,
1034
+ audioPolicy: timedStream.kind === "video" ? "mute" : "preserve-pitch",
1035
+ },
1036
+ }];
1037
+ }
1038
+ const manifest = await engine.files.manifest(artifact.value.artifactId);
1039
+ if (!manifest.ok)
1040
+ throw new Error(manifest.error.message);
1041
+ const media = findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
1042
+ if (!media)
1043
+ throw new Error("The artifact has no insertable source media");
1044
+ const track = requiredTrack(sequence, request.track_id, "video");
1045
+ return [{
1046
+ kind: "insert-clip",
1047
+ mode: request.mode ?? "insert",
1048
+ placement: {
1049
+ trackId: track.trackId,
1050
+ timelineStartFrame: request.timeline_start_frame,
1051
+ durationFrames: request.duration_frames ?? 90,
1052
+ source: {
1053
+ kind: "still",
1054
+ artifactId: artifact.value.artifactId,
1055
+ sourcePath: media.name,
1056
+ objectHash: media.objectHash,
1057
+ },
1058
+ },
1059
+ }];
1060
+ }
1061
+ async function buildSequenceEditPlan(engine, input, sourceSurface, actor, expectedRevision) {
1062
+ const sequence = engine.sequences.getPrimary();
1063
+ const contentRevision = sequenceContinuationRevision(sequence);
1064
+ if (expectedRevision
1065
+ && expectedRevision !== sequence.revision
1066
+ && expectedRevision !== contentRevision) {
1067
+ throw new Error(`Sequence revision changed: expected ${expectedRevision}, current ${contentRevision}`);
1068
+ }
1069
+ const commandId = uuidv7();
1070
+ const requests = Array.isArray(input) ? input : [input];
1071
+ const appendsOnly = requests.every((request) => request.kind === "append");
1072
+ if (!appendsOnly && requests.some((request) => request.kind === "append")) {
1073
+ throw new Error("Append batches can only contain append requests; plan other edits separately");
1074
+ }
1075
+ const operations = (await Promise.all(requests.map((request) => operationsForRequest(engine, sequence, request)))).flat();
1076
+ if (appendsOnly) {
1077
+ const trackEnds = new Map();
1078
+ for (const operation of operations) {
1079
+ if (operation.kind !== "insert-clip")
1080
+ continue;
1081
+ const placement = operation.placement;
1082
+ placement.timelineStartFrame = trackEnds.get(placement.trackId) ?? placement.timelineStartFrame;
1083
+ trackEnds.set(placement.trackId, placement.timelineStartFrame + placement.durationFrames);
1084
+ }
1085
+ }
1086
+ const confirmationRequired = requests.length > 1 || operations.length > 1;
1087
+ const intent = {
1088
+ intentVersion: MVP_CONTRACT_VERSION,
1089
+ commandId,
1090
+ sequenceId: sequence.sequenceId,
1091
+ baseRevision: engine.head,
1092
+ actor,
1093
+ sourceSurface,
1094
+ confirmationPolicy: "reversible-single-step",
1095
+ operations,
1096
+ };
1097
+ const summary = requests.length === 1
1098
+ ? `${requests[0].kind.slice(0, 1).toUpperCase()}${requests[0].kind.slice(1)} sequence edit`
1099
+ : `Apply ${requests.length} ordered sequence edits`;
1100
+ return {
1101
+ planVersion: 1,
1102
+ planId: commandId,
1103
+ sourceSurface,
1104
+ actor,
1105
+ bookId: engine.book.get().bookId,
1106
+ summary,
1107
+ normalizedReferences: [
1108
+ {
1109
+ kind: "sequence",
1110
+ targetId: sequence.sequenceId,
1111
+ revision: engine.head,
1112
+ },
1113
+ ],
1114
+ stages: [{
1115
+ stageId: `${commandId}:edit:0`,
1116
+ kind: "edit",
1117
+ summary: requests.map((request) => request.kind).join(" → "),
1118
+ intent,
1119
+ }],
1120
+ confirmation: {
1121
+ required: confirmationRequired,
1122
+ policy: confirmationRequired ? "always" : "reversible-single-step",
1123
+ reason: confirmationRequired
1124
+ ? "This plan contains multiple ordered operations"
1125
+ : "One reversible edit can apply immediately",
1126
+ },
1127
+ };
1128
+ }
1129
+ function clipIdFromText(sequence, value) {
1130
+ const token = value.replace(/^@/u, "").toLowerCase();
1131
+ const matches = sequence.clips.filter((clip) => clip.clipId.toLowerCase().startsWith(token));
1132
+ return matches.length === 1 ? matches[0].clipId : null;
1133
+ }
1134
+ function trackIdFromText(sequence, value) {
1135
+ const token = value.replace(/^@/u, "").toLowerCase();
1136
+ const matches = sequence.tracks.filter((track) => track.trackId.toLowerCase().startsWith(token)
1137
+ || track.name.toLowerCase() === token);
1138
+ return matches.length === 1 ? matches[0].trackId : null;
1139
+ }
1140
+ function integerMatch(text, expression) {
1141
+ const match = expression.exec(text);
1142
+ if (!match)
1143
+ return null;
1144
+ const value = Number(match[1]);
1145
+ return Number.isSafeInteger(value) ? value : null;
1146
+ }
1147
+ async function parseChatEditClause(engine, sequence, clause, mediaContext) {
1148
+ const normalized = clause.trim();
1149
+ if (/\b(insert|add|place)\b/iu.test(normalized)
1150
+ && !/\bgap\b/iu.test(normalized)) {
1151
+ const reference = scanNotebookMentions(normalized)[0]?.reference;
1152
+ const startFrame = integerMatch(normalized, /\b(?:at|from|starting\s+at)\s+(?:frame\s+)?(\d+)\b/iu);
1153
+ const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
1154
+ if ((!reference && !mediaContext) || startFrame === null) {
1155
+ return "Tell me which @asset to insert and the destination frame.";
1156
+ }
1157
+ if (!reference && mediaContext) {
1158
+ return {
1159
+ kind: "insert",
1160
+ artifact_id: mediaContext.artifactId,
1161
+ timeline_start_frame: startFrame,
1162
+ ...(durationFrames === null ? {} : { duration_frames: durationFrames }),
1163
+ };
1164
+ }
1165
+ const resolved = await resolveArtifactRefs(engine, [reference]);
1166
+ if (resolved.unresolved.length > 0) {
1167
+ return `I could not resolve @${reference}. Choose a source from the library.`;
1168
+ }
1169
+ const artifact = resolved.resolved[0];
1170
+ if (!artifact)
1171
+ return `I could not resolve @${reference}.`;
1172
+ return {
1173
+ kind: "insert",
1174
+ artifact_id: artifact.artifactId,
1175
+ timeline_start_frame: startFrame,
1176
+ ...(durationFrames === null ? {} : { duration_frames: durationFrames }),
1177
+ };
1178
+ }
1179
+ if (/\bmove\b/iu.test(normalized)) {
1180
+ const references = normalized.match(/@?[0-9a-f-]{8,}/giu) ?? [];
1181
+ const clipId = references[0]
1182
+ ? clipIdFromText(sequence, references[0])
1183
+ : null;
1184
+ const trackId = references[1]
1185
+ ? trackIdFromText(sequence, references[1])
1186
+ : null;
1187
+ const startFrame = integerMatch(normalized, /\b(?:at|to|from)\s+(?:frame\s+)?(\d+)\b/iu);
1188
+ if (!clipId || !trackId || startFrame === null) {
1189
+ return "Tell me the clip ID, destination track ID, and destination frame.";
1190
+ }
1191
+ return {
1192
+ kind: "move",
1193
+ clip_id: clipId,
1194
+ track_id: trackId,
1195
+ timeline_start_frame: startFrame,
1196
+ };
1197
+ }
1198
+ if (/\bsplit\b/iu.test(normalized)) {
1199
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1200
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1201
+ const splitFrame = integerMatch(normalized, /\b(?:at|on)\s+(?:frame\s+)?(\d+)\b/iu);
1202
+ if (!clipId || splitFrame === null) {
1203
+ return "Tell me the clip ID and sequence frame where it should split.";
1204
+ }
1205
+ return { kind: "split", clip_id: clipId, split_frame: splitFrame };
1206
+ }
1207
+ if (/\b(delete|remove)\b/iu.test(normalized)) {
1208
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1209
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1210
+ if (!clipId)
1211
+ return "Tell me which clip ID to delete.";
1212
+ return {
1213
+ kind: "delete",
1214
+ clip_id: clipId,
1215
+ ripple: /\bripple\b/iu.test(normalized),
1216
+ };
1217
+ }
1218
+ if (/\bgap\b/iu.test(normalized)) {
1219
+ const startFrame = integerMatch(normalized, /\b(?:at|from)\s+(?:frame\s+)?(\d+)\b/iu);
1220
+ const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
1221
+ if (startFrame === null || durationFrames === null) {
1222
+ return "Tell me the gap start frame and duration in frames.";
1223
+ }
1224
+ return {
1225
+ kind: "gap",
1226
+ timeline_start_frame: startFrame,
1227
+ duration_frames: durationFrames,
1228
+ };
1229
+ }
1230
+ if (/\b(fit|fill|crop)\b/iu.test(normalized)) {
1231
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1232
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1233
+ const fit = /\bfit\b/iu.test(normalized)
1234
+ ? "fit"
1235
+ : /\bcrop\b/iu.test(normalized)
1236
+ ? "crop"
1237
+ : "fill";
1238
+ if (!clipId)
1239
+ return "Tell me which clip ID to fit, fill, or crop.";
1240
+ return { kind: "transform", clip_id: clipId, fit };
1241
+ }
1242
+ if (/\breframe\b/iu.test(normalized)) {
1243
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1244
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1245
+ if (!clipId)
1246
+ return "Tell me which visual clip ID to reframe.";
1247
+ return { kind: "reframe", clip_id: clipId };
1248
+ }
1249
+ if (/\breverse\b/iu.test(normalized)) {
1250
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1251
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1252
+ const clip = clipId
1253
+ ? sequence.clips.find((candidate) => candidate.clipId === clipId)
1254
+ : null;
1255
+ if (!clip || clip.source.kind !== "timed") {
1256
+ return "Tell me which timed clip ID to reverse.";
1257
+ }
1258
+ const timed = clip;
1259
+ return {
1260
+ kind: "speed",
1261
+ clip_id: clip.clipId,
1262
+ numerator: timed.speed.numerator,
1263
+ denominator: timed.speed.denominator,
1264
+ reverse: !timed.reverse,
1265
+ audio_policy: timed.audioPolicy,
1266
+ };
1267
+ }
1268
+ if (/\b(dissolve|transition)\b/iu.test(normalized)) {
1269
+ const references = normalized.match(/@?[0-9a-f-]{8,}/giu) ?? [];
1270
+ const outgoing = references[0]
1271
+ ? clipIdFromText(sequence, references[0])
1272
+ : null;
1273
+ const incoming = references[1]
1274
+ ? clipIdFromText(sequence, references[1])
1275
+ : null;
1276
+ const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
1277
+ if (!outgoing || !incoming) {
1278
+ return "Tell me the outgoing and incoming clip IDs for the transition.";
1279
+ }
1280
+ return {
1281
+ kind: "transition",
1282
+ outgoing_clip_id: outgoing,
1283
+ incoming_clip_id: incoming,
1284
+ transition_kind: /\bdissolve\b/iu.test(normalized) ? "dissolve" : "cut",
1285
+ ...(durationFrames === null ? {} : { duration_frames: durationFrames }),
1286
+ };
1287
+ }
1288
+ if (/\bcaption\b/iu.test(normalized)) {
1289
+ const startFrame = integerMatch(normalized, /\b(?:at|from|starting\s+at)\s+(?:frame\s+)?(\d+)\b/iu);
1290
+ const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
1291
+ const style = /\bhigh-contrast\b/iu.test(normalized)
1292
+ ? "high-contrast"
1293
+ : /\bbold\b/iu.test(normalized)
1294
+ ? "bold"
1295
+ : "clean";
1296
+ const quoted = /["“]([^"”]+)["”]/u.exec(normalized)?.[1]?.trim();
1297
+ const unquoted = normalized
1298
+ .replace(/^.*?\bcaption\b/iu, "")
1299
+ .replace(/\b(?:at|from|starting\s+at)\s+(?:frame\s+)?\d+\b/giu, "")
1300
+ .replace(/\b(?:for|duration(?:\s+of)?)\s+\d+\s*(?:frames?)?\b/giu, "")
1301
+ .replace(/\b(?:clean|bold|high-contrast)\b/giu, "")
1302
+ .replace(/^[\s:,-]+|[\s:,-]+$/gu, "");
1303
+ const text = quoted ?? unquoted;
1304
+ if (startFrame === null || durationFrames === null || !text) {
1305
+ return "Tell me the caption text, start frame, and duration in frames.";
1306
+ }
1307
+ return {
1308
+ kind: "caption",
1309
+ timeline_start_frame: startFrame,
1310
+ duration_frames: durationFrames,
1311
+ text,
1312
+ style_id: style,
1313
+ };
1314
+ }
1315
+ if (/\btrim\b/iu.test(normalized)) {
1316
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1317
+ const startFrame = integerMatch(normalized, /\b(?:at|from|starting\s+at)\s+(?:frame\s+)?(\d+)\b/iu);
1318
+ const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
1319
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1320
+ if (!clipId || startFrame === null || durationFrames === null) {
1321
+ return "Tell me the clip ID, destination frame, and duration in frames.";
1322
+ }
1323
+ return {
1324
+ kind: "trim",
1325
+ clip_id: clipId,
1326
+ timeline_start_frame: startFrame,
1327
+ duration_frames: durationFrames,
1328
+ };
1329
+ }
1330
+ if (/\b(volume|gain|louder|quieter|mute)\b/iu.test(normalized)) {
1331
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1332
+ const gainMatch = /(-?\d+(?:\.\d+)?)\s*(?:db)?\b/iu.exec(normalized.replace(reference ?? "", ""));
1333
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1334
+ const gainDb = /\bmute\b/iu.test(normalized)
1335
+ ? -96
1336
+ : gainMatch
1337
+ ? Number(gainMatch[1])
1338
+ : Number.NaN;
1339
+ if (!clipId || !Number.isFinite(gainDb) || gainDb < -96 || gainDb > 24) {
1340
+ return "Tell me the audio clip ID and gain from -96 to 24 dB.";
1341
+ }
1342
+ return {
1343
+ kind: "volume",
1344
+ clip_id: clipId,
1345
+ gain_db: gainDb,
1346
+ ...(gainDb === -96 ? { muted: true } : {}),
1347
+ };
1348
+ }
1349
+ if (/\bduck\b/iu.test(normalized)) {
1350
+ const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
1351
+ const clipId = reference ? clipIdFromText(sequence, reference) : null;
1352
+ const reduction = /(-\d+(?:\.\d+)?)\s*(?:db)?\b/iu.exec(normalized.replace(reference ?? "", ""));
1353
+ const reductionDb = reduction ? Number(reduction[1]) : -12;
1354
+ if (!clipId || reductionDb < -36 || reductionDb > -1) {
1355
+ return "Tell me the narration clip ID and a reduction from -36 to -1 dB.";
1356
+ }
1357
+ return {
1358
+ kind: "duck",
1359
+ narration_clip_id: clipId,
1360
+ reduction_db: reductionDb,
1361
+ };
1362
+ }
1363
+ return UNSUPPORTED_CHAT_EDIT_MESSAGE;
1364
+ }
1365
+ async function parseChatEditRequest(engine, prompt, mediaContext) {
1366
+ const clauses = prompt.split(/\s*(?:;|\band\s+then\b|\bthen\b)\s*/iu)
1367
+ .filter((clause) => clause.trim().length > 0);
1368
+ if (clauses.length === 0) {
1369
+ return {
1370
+ kind: "clarification",
1371
+ message: "Describe the edit you want to make.",
1372
+ reason: "incomplete",
1373
+ };
1374
+ }
1375
+ const sequence = engine.sequences.getPrimary();
1376
+ const requests = [];
1377
+ for (const clause of clauses) {
1378
+ const parsed = await parseChatEditClause(engine, sequence, clause, mediaContext);
1379
+ if (typeof parsed === "string") {
1380
+ return {
1381
+ kind: "clarification",
1382
+ message: parsed,
1383
+ reason: parsed === UNSUPPORTED_CHAT_EDIT_MESSAGE
1384
+ ? "unsupported"
1385
+ : "incomplete",
1386
+ };
1387
+ }
1388
+ requests.push(parsed);
1389
+ }
1390
+ return { kind: "plan", requests };
1391
+ }
1392
+ export function buildGenerationInsertPlan(engine, input, sourceSurface, actor) {
1393
+ if (input.estimated_cost_usd === undefined
1394
+ && input.estimate_unavailable !== true) {
1395
+ throw new Error("Generation requires a cost estimate or unavailable warning");
1396
+ }
1397
+ const sequence = engine.sequences.getPrimary();
1398
+ const track = sequence.tracks.find((candidate) => candidate.trackId === input.track_id);
1399
+ if (!track || track.locked || !track.enabled) {
1400
+ throw new Error("Generation destination track is not editable");
1401
+ }
1402
+ const planId = uuidv7();
1403
+ return {
1404
+ planVersion: 1,
1405
+ planId,
1406
+ sourceSurface,
1407
+ actor,
1408
+ bookId: engine.book.get().bookId,
1409
+ summary: `Generate with ${input.model} and insert the result`,
1410
+ normalizedReferences: [{
1411
+ kind: "sequence",
1412
+ targetId: sequence.sequenceId,
1413
+ revision: sequenceContinuationRevision(sequence),
1414
+ }],
1415
+ stages: [{
1416
+ stageId: `${planId}:queued:0`,
1417
+ kind: "queued",
1418
+ summary: `Run ${input.tool}`,
1419
+ jobType: "generation",
1420
+ tool: input.tool,
1421
+ input: input.input,
1422
+ provider: input.provider,
1423
+ model: input.model,
1424
+ ...(input.estimated_cost_usd === undefined
1425
+ ? { estimateUnavailable: true }
1426
+ : { estimatedCostUsd: input.estimated_cost_usd }),
1427
+ insertsOutput: true,
1428
+ continuation: {
1429
+ sequenceId: sequence.sequenceId,
1430
+ baseRevision: sequenceContinuationRevision(sequence),
1431
+ destination: {
1432
+ trackId: track.trackId,
1433
+ timelineStartFrame: input.timeline_start_frame,
1434
+ ...(input.duration_frames === undefined
1435
+ ? {}
1436
+ : { durationFrames: input.duration_frames }),
1437
+ },
1438
+ },
1439
+ }],
1440
+ confirmation: {
1441
+ required: true,
1442
+ policy: "always",
1443
+ reason: "This plan can incur provider spend",
1444
+ },
1445
+ };
1446
+ }
1447
+ export async function continueQueuedInsert(engine, plan, outputArtifactId, actor) {
1448
+ const stages = Array.isArray(plan.stages) ? plan.stages : [];
1449
+ const stage = stages.find((candidate) => typeof candidate === "object"
1450
+ && candidate !== null
1451
+ && candidate.kind === "queued");
1452
+ const continuation = stage && typeof stage.continuation === "object"
1453
+ && stage.continuation !== null
1454
+ ? stage.continuation
1455
+ : null;
1456
+ const destination = continuation
1457
+ && typeof continuation.destination === "object"
1458
+ && continuation.destination !== null
1459
+ ? continuation.destination
1460
+ : null;
1461
+ const sequenceId = continuation?.sequenceId;
1462
+ const baseRevision = continuation?.baseRevision;
1463
+ if (stage?.insertsOutput !== true
1464
+ || typeof sequenceId !== "string"
1465
+ || typeof baseRevision !== "string"
1466
+ || !destination) {
1467
+ throw new Error("The plan has no revision-pinned queued insertion");
1468
+ }
1469
+ const sequence = engine.sequences.getPrimary();
1470
+ const currentRevision = sequenceContinuationRevision(sequence);
1471
+ if (sequence.sequenceId !== sequenceId || currentRevision !== baseRevision) {
1472
+ return {
1473
+ kind: "stale-continuation",
1474
+ message: "The sequence changed while generation was running. Review a new destination.",
1475
+ expectedRevision: baseRevision,
1476
+ currentRevision,
1477
+ outputArtifactId,
1478
+ };
1479
+ }
1480
+ const trackId = destination.trackId;
1481
+ const timelineStartFrame = destination.timelineStartFrame;
1482
+ const durationFrames = destination.durationFrames;
1483
+ if (typeof trackId !== "string"
1484
+ || typeof timelineStartFrame !== "number"
1485
+ || (durationFrames !== undefined
1486
+ && typeof durationFrames !== "number")) {
1487
+ throw new Error("The queued insertion destination is invalid");
1488
+ }
1489
+ const next = await buildSequenceEditPlan(engine, {
1490
+ kind: "insert",
1491
+ artifact_id: outputArtifactId,
1492
+ track_id: trackId,
1493
+ timeline_start_frame: timelineStartFrame,
1494
+ ...(durationFrames === undefined ? {} : { duration_frames: durationFrames }),
1495
+ }, "system", actor);
1496
+ next.parentActionId = typeof plan.planId === "string" ? plan.planId : undefined;
1497
+ return next;
1498
+ }
1499
+ export function commandPlanDefs(engine) {
1500
+ return [
1501
+ {
1502
+ name: "plan_sequence_edit",
1503
+ description: "Compile one or more sequence edits (insert, trim, move, volume, captions, ...) into one reviewable CommandPlan",
1504
+ scope: "book",
1505
+ schema: {
1506
+ ...bookSchema,
1507
+ source_surface: z.enum(["ui", "slash", "chat"]),
1508
+ actor: z.string().min(1),
1509
+ expected_revision: z.string().min(1).optional(),
1510
+ edits: z.array(editRequestSchema).min(1).max(20),
1511
+ },
1512
+ handler: async (args) => {
1513
+ try {
1514
+ return json(await buildSequenceEditPlan(engine, args.edits, args.source_surface, String(args.actor), typeof args.expected_revision === "string"
1515
+ ? args.expected_revision
1516
+ : undefined));
1517
+ }
1518
+ catch (error) {
1519
+ return failure(error instanceof Error ? error.message : String(error));
1520
+ }
1521
+ },
1522
+ },
1523
+ {
1524
+ name: "plan_chat_command",
1525
+ description: "Parse a natural-language sequence edit into a typed CommandPlan without executing tools",
1526
+ scope: "book",
1527
+ schema: {
1528
+ ...bookSchema,
1529
+ actor: z.string().min(1),
1530
+ prompt: z.string().min(1),
1531
+ media_context: z.preprocess((value) => {
1532
+ if (!Array.isArray(value))
1533
+ return value;
1534
+ return value.length === 1 ? value[0] : undefined;
1535
+ }, z.object({
1536
+ notebook_id: z.string().uuid(),
1537
+ cell_id: z.string().uuid(),
1538
+ artifact_id: z.string().uuid(),
1539
+ }).optional()).describe("Single fullscreen clip {notebook_id, cell_id, artifact_id}; never an array"),
1540
+ },
1541
+ handler: async (args) => {
1542
+ try {
1543
+ let mediaContext;
1544
+ if (args.media_context) {
1545
+ const context = args.media_context;
1546
+ const notebook = engine.notebooks.read(context.notebook_id);
1547
+ if (!notebook.ok)
1548
+ throw new Error(notebook.error.message);
1549
+ const cell = notebook.value.cells.find((candidate) => candidate.id === context.cell_id);
1550
+ if (!cell || (cell.type !== "image" && cell.type !== "video")) {
1551
+ throw new Error("Fullscreen media context must reference an image or video cell");
1552
+ }
1553
+ if (cell.outputArtifactId !== context.artifact_id) {
1554
+ throw new Error("Fullscreen media context does not match the cell output");
1555
+ }
1556
+ const artifact = engine.artifacts.get(context.artifact_id);
1557
+ if (!artifact.ok)
1558
+ throw new Error(artifact.error.message);
1559
+ mediaContext = { artifactId: artifact.value.artifactId };
1560
+ }
1561
+ const parsed = await parseChatEditRequest(engine, String(args.prompt), mediaContext);
1562
+ if (parsed.kind === "clarification")
1563
+ return json(parsed);
1564
+ return json(await buildSequenceEditPlan(engine, parsed.requests, "chat", String(args.actor)));
1565
+ }
1566
+ catch (error) {
1567
+ return failure(error instanceof Error ? error.message : String(error));
1568
+ }
1569
+ },
1570
+ },
1571
+ {
1572
+ name: "plan_generation_insert",
1573
+ description: "Plan a paid queued generation with provider, model, cost, and a revision-pinned insertion",
1574
+ scope: "book",
1575
+ schema: {
1576
+ ...bookSchema,
1577
+ source_surface: z.enum(["ui", "slash", "chat"]),
1578
+ actor: z.string().min(1),
1579
+ generation: generationPlanSchema,
1580
+ },
1581
+ handler: async (args) => {
1582
+ try {
1583
+ return json(buildGenerationInsertPlan(engine, args.generation, args.source_surface, String(args.actor)));
1584
+ }
1585
+ catch (error) {
1586
+ return failure(error instanceof Error ? error.message : String(error));
1587
+ }
1588
+ },
1589
+ },
1590
+ {
1591
+ name: "continue_queued_insert",
1592
+ description: "Turn a completed generation into an insertion plan only when its pinned sequence destination is still current",
1593
+ scope: "book",
1594
+ schema: {
1595
+ ...bookSchema,
1596
+ plan: z.record(z.string(), z.unknown()),
1597
+ output_artifact_id: z.string().uuid(),
1598
+ actor: z.string().min(1),
1599
+ },
1600
+ handler: async (args) => {
1601
+ try {
1602
+ return json(await continueQueuedInsert(engine, args.plan, String(args.output_artifact_id), String(args.actor)));
1603
+ }
1604
+ catch (error) {
1605
+ return failure(error instanceof Error ? error.message : String(error));
1606
+ }
1607
+ },
1608
+ },
1609
+ {
1610
+ name: "preview_edit_intent",
1611
+ description: "Preview one CommandPlan edit stage through the canonical engine diff",
1612
+ scope: "book",
1613
+ schema: {
1614
+ ...bookSchema,
1615
+ intent: intentSchema,
1616
+ },
1617
+ handler: async (args) => {
1618
+ const preview = engine.edits.preview(args.intent);
1619
+ return preview.ok ? json(preview.value) : failure(preview.error.message);
1620
+ },
1621
+ },
1622
+ {
1623
+ name: "commit_edit_intent",
1624
+ description: "Commit an unchanged previewed CommandPlan edit stage atomically",
1625
+ scope: "book",
1626
+ schema: {
1627
+ ...bookSchema,
1628
+ intent: intentSchema,
1629
+ preview_hash: z.string().min(1),
1630
+ },
1631
+ handler: async (args) => {
1632
+ const committed = await engine.edits.commit(args.intent, String(args.preview_hash));
1633
+ return committed.ok ? json(committed.value) : failure(committed.error.message);
1634
+ },
1635
+ },
1636
+ {
1637
+ name: "get_edit_action",
1638
+ description: "Read the immutable audit record for a committed CommandPlan edit stage",
1639
+ scope: "book",
1640
+ schema: {
1641
+ ...bookSchema,
1642
+ action_id: z.string().uuid(),
1643
+ },
1644
+ handler: async (args) => {
1645
+ const action = engine.edits.get(String(args.action_id));
1646
+ return action.ok ? json(action.value) : failure(action.error.message);
1647
+ },
1648
+ },
1649
+ {
1650
+ name: "restore_edit_action",
1651
+ description: "Create a forward revision restoring a prior edit action or revision",
1652
+ scope: "book",
1653
+ schema: {
1654
+ ...bookSchema,
1655
+ target_action_id: z.string().uuid().optional(),
1656
+ target_revision: z.string().min(1).optional(),
1657
+ base_revision: z.string().min(1),
1658
+ actor: z.string().min(1),
1659
+ source_surface: z.enum(["ui", "slash", "chat"]),
1660
+ },
1661
+ handler: async (args) => {
1662
+ if (!args.target_action_id && !args.target_revision) {
1663
+ return failure("A target action or target revision is required");
1664
+ }
1665
+ const restored = await engine.edits.restore({
1666
+ ...(typeof args.target_action_id === "string"
1667
+ ? { targetActionId: args.target_action_id }
1668
+ : {}),
1669
+ ...(typeof args.target_revision === "string"
1670
+ ? { targetRevision: args.target_revision }
1671
+ : {}),
1672
+ baseRevision: String(args.base_revision),
1673
+ actor: String(args.actor),
1674
+ sourceSurface: args.source_surface,
1675
+ });
1676
+ return restored.ok ? json(restored.value) : failure(restored.error.message);
1677
+ },
1678
+ },
1679
+ ];
1680
+ }