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,365 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { extname, join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { runFfmpeg, probeVideo, AUDIO_OPTS, H264_MEDIUM } from "../ffmpeg.js";
6
+ import { registerJobHandler } from "../queue.js";
7
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
8
+ import { sequenceExportDimensions } from "../tools/export-sequence.js";
9
+ function rational(value) {
10
+ return value.numerator / value.denominator;
11
+ }
12
+ function secondsForFrames(sequence, frames) {
13
+ return frames / rational(sequence.frameRate);
14
+ }
15
+ function sourceSeconds(clip) {
16
+ if (clip.source.kind === "still")
17
+ return { start: 0, duration: 0 };
18
+ const timeBase = rational(clip.source.range.timeBase);
19
+ return {
20
+ start: clip.source.range.startTick * timeBase,
21
+ duration: clip.source.range.durationTicks * timeBase,
22
+ };
23
+ }
24
+ function escapeFilterPath(path) {
25
+ return path.replace(/\\/gu, "\\\\").replace(/:/gu, "\\:").replace(/'/gu, "\\'");
26
+ }
27
+ function captionTimestamp(seconds) {
28
+ const totalMilliseconds = Math.max(0, Math.round(seconds * 1000));
29
+ const hours = Math.floor(totalMilliseconds / 3_600_000);
30
+ const minutes = Math.floor(totalMilliseconds % 3_600_000 / 60_000);
31
+ const secs = Math.floor(totalMilliseconds % 60_000 / 1000);
32
+ const milliseconds = totalMilliseconds % 1000;
33
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}.${String(milliseconds).padStart(3, "0")}`;
34
+ }
35
+ function webVtt(sequence) {
36
+ const cues = [...sequence.captions].sort((left, right) => left.timelineStartFrame - right.timelineStartFrame);
37
+ const body = cues.map((cue) => {
38
+ const start = secondsForFrames(sequence, cue.timelineStartFrame);
39
+ const end = secondsForFrames(sequence, cue.timelineStartFrame + cue.durationFrames);
40
+ return `${captionTimestamp(start)} --> ${captionTimestamp(end)}\n${cue.text}`;
41
+ }).join("\n\n");
42
+ return `WEBVTT\n\n${body}${body ? "\n" : ""}`;
43
+ }
44
+ async function resolveClips(engine, sequence, workspace, signal) {
45
+ const tracks = new Map(sequence.tracks.map((track) => [track.trackId, track]));
46
+ const resolved = [];
47
+ for (const clip of sequence.clips.filter((candidate) => candidate.enabled)) {
48
+ const track = tracks.get(clip.trackId);
49
+ if (!track || !track.enabled || track.kind === "caption")
50
+ continue;
51
+ signal.throwIfAborted();
52
+ const expectedHash = clip.source.kind === "timed"
53
+ ? clip.source.range.objectHash
54
+ : clip.source.objectHash;
55
+ let sourcePath;
56
+ if (clip.source.kind === "timed") {
57
+ const stream = engine.streams.getAtRevision(clip.source.range.streamId, sequence.revision);
58
+ if (!stream.ok)
59
+ throw new Error(stream.error.message);
60
+ if (stream.value.artifactId !== clip.source.artifactId || stream.value.objectHash !== expectedHash) {
61
+ throw new Error(`Source stream does not match the confirmed clip ${clip.clipId}`);
62
+ }
63
+ sourcePath = stream.value.sourcePath;
64
+ }
65
+ else {
66
+ sourcePath = clip.source.sourcePath;
67
+ }
68
+ const snapshot = await engine.files.readAtRevision(clip.source.artifactId, sourcePath, sequence.revision);
69
+ if (!snapshot.ok)
70
+ throw new Error(snapshot.error.message);
71
+ if (createHash("sha256").update(snapshot.value).digest("hex") !== expectedHash) {
72
+ throw new Error(`Source object does not match the confirmed clip ${clip.clipId}`);
73
+ }
74
+ const path = join(workspace, `input-${resolved.length}${extname(sourcePath)}`);
75
+ await writeFile(path, snapshot.value, { signal });
76
+ const probe = await probeVideo(path, { signal });
77
+ resolved.push({
78
+ clip,
79
+ track,
80
+ path,
81
+ inputIndex: resolved.length + 1,
82
+ hasVideo: probe.videoStream !== null,
83
+ hasAudio: probe.hasAudio,
84
+ });
85
+ }
86
+ return resolved;
87
+ }
88
+ function inputArgs(sequence, resolved, width, height) {
89
+ const duration = secondsForFrames(sequence, Math.max(...sequence.clips.map((clip) => clip.timelineStartFrame + clip.durationFrames)));
90
+ const fps = rational(sequence.frameRate);
91
+ const args = [
92
+ "-f", "lavfi",
93
+ "-i", `color=c=black:s=${width}x${height}:r=${fps}:d=${duration}`,
94
+ ];
95
+ for (const item of resolved) {
96
+ const outputDuration = secondsForFrames(sequence, item.clip.durationFrames);
97
+ const source = sourceSeconds(item.clip);
98
+ if (item.clip.source.kind === "still") {
99
+ args.push("-loop", "1", "-t", String(outputDuration), "-i", item.path);
100
+ }
101
+ else {
102
+ args.push("-ss", String(source.start), "-t", String(source.duration), "-i", item.path);
103
+ }
104
+ }
105
+ return args;
106
+ }
107
+ function videoFilter(sequence, resolved, width, height) {
108
+ const fps = rational(sequence.frameRate);
109
+ const filters = [
110
+ `[0:v]scale=${width}:${height},fps=${fps},format=yuv420p[vbase]`,
111
+ ];
112
+ let previous = "vbase";
113
+ const visual = resolved
114
+ .filter((item) => item.track.kind === "video" && item.hasVideo)
115
+ .sort((left, right) => left.track.ordinal - right.track.ordinal
116
+ || left.clip.timelineStartFrame - right.clip.timelineStartFrame);
117
+ visual.forEach((item, index) => {
118
+ const transform = item.clip.transform;
119
+ const fit = transform?.fit ?? "fit";
120
+ const crop = transform
121
+ ? `crop=iw*${1 - transform.cropLeft - transform.cropRight}:ih*${1 - transform.cropTop - transform.cropBottom}:iw*${transform.cropLeft}:ih*${transform.cropTop},`
122
+ : "";
123
+ const scale = fit === "fit"
124
+ ? `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:color=black@0`
125
+ : `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}`;
126
+ const duration = secondsForFrames(sequence, item.clip.durationFrames);
127
+ const start = secondsForFrames(sequence, item.clip.timelineStartFrame);
128
+ const source = sourceSeconds(item.clip);
129
+ const sourceDuration = item.clip.source.kind === "timed"
130
+ ? source.duration
131
+ : duration;
132
+ const playbackRate = item.clip.source.kind === "timed"
133
+ ? item.clip.speed.numerator
134
+ / item.clip.speed.denominator
135
+ : 1;
136
+ const clipLabel = `vclip${index}`;
137
+ const outputLabel = `vstage${index}`;
138
+ const opacity = transform?.opacity ?? 1;
139
+ const reverse = item.clip.source.kind === "timed"
140
+ && item.clip.reverse
141
+ ? "reverse,"
142
+ : "";
143
+ const transformFilters = transform
144
+ ? `,scale=iw*${transform.scaleX}:ih*${transform.scaleY},rotate=${transform.rotationDegrees}*PI/180:ow=rotw(iw):oh=roth(ih):c=none`
145
+ : "";
146
+ const transition = clipTransitions(sequence, item.clip);
147
+ const fadeIn = transition.incoming
148
+ ? `,fade=t=in:st=0:d=${secondsForFrames(sequence, transition.incoming.durationFrames)}:alpha=1`
149
+ : "";
150
+ const fadeOut = transition.outgoing
151
+ ? `,fade=t=out:st=${Math.max(0, duration - secondsForFrames(sequence, transition.outgoing.durationFrames))}:d=${secondsForFrames(sequence, transition.outgoing.durationFrames)}:alpha=1`
152
+ : "";
153
+ filters.push(`[${item.inputIndex}:v]${crop}${reverse}${scale}${transformFilters},fps=${fps},trim=duration=${sourceDuration},setpts=(PTS-STARTPTS)/${playbackRate},tpad=stop_mode=clone:stop_duration=${duration},trim=duration=${duration},setpts=PTS+${start}/TB,format=rgba,colorchannelmixer=aa=${opacity}${fadeIn}${fadeOut}[${clipLabel}]`);
154
+ const x = `(W-w)/2+${transform?.positionX ?? 0}`;
155
+ const y = `(H-h)/2+${transform?.positionY ?? 0}`;
156
+ filters.push(`[${previous}][${clipLabel}]overlay=${x}:${y}:enable='between(t,${start},${start + duration})'[${outputLabel}]`);
157
+ previous = outputLabel;
158
+ });
159
+ return { filters, output: previous };
160
+ }
161
+ function clipTransitions(sequence, clip) {
162
+ return {
163
+ incoming: sequence.transitions.find((transition) => transition.kind === "dissolve" && transition.incomingClipId === clip.clipId),
164
+ outgoing: sequence.transitions.find((transition) => transition.kind === "dissolve" && transition.outgoingClipId === clip.clipId),
165
+ };
166
+ }
167
+ function atempoFilters(speed) {
168
+ const filters = [];
169
+ let remaining = speed;
170
+ while (remaining > 2) {
171
+ filters.push("atempo=2");
172
+ remaining /= 2;
173
+ }
174
+ while (remaining < 0.5) {
175
+ filters.push("atempo=0.5");
176
+ remaining /= 0.5;
177
+ }
178
+ if (Math.abs(remaining - 1) > 0.000001) {
179
+ filters.push(`atempo=${remaining}`);
180
+ }
181
+ return filters;
182
+ }
183
+ function audioFilter(sequence, resolved, duration) {
184
+ const filters = [
185
+ `anullsrc=r=${sequence.audioSampleRateHz}:cl=stereo,atrim=duration=${duration}[asilence]`,
186
+ ];
187
+ const audio = resolved.filter((item) => {
188
+ if (!item.hasAudio)
189
+ return false;
190
+ if (item.track.kind === "audio")
191
+ return !item.track.muted;
192
+ const clip = item.clip;
193
+ if (item.track.kind !== "video" || clip.source.kind !== "timed")
194
+ return false;
195
+ return clip.audioPolicy !== "mute";
196
+ });
197
+ const labels = ["[asilence]"];
198
+ audio.forEach((item, index) => {
199
+ const clipDuration = secondsForFrames(sequence, item.clip.durationFrames);
200
+ const start = secondsForFrames(sequence, item.clip.timelineStartFrame);
201
+ const audioSettings = item.clip.audio;
202
+ const timed = item.clip;
203
+ const speed = timed.speed.numerator / timed.speed.denominator;
204
+ const gain = audioSettings?.muted
205
+ ? 0
206
+ : 10 ** ((audioSettings?.gainDb ?? 0) / 20);
207
+ const transitions = clipTransitions(sequence, item.clip);
208
+ const antiClickFade = Math.min(0.005, clipDuration / 2);
209
+ const fadeIn = Math.max(antiClickFade, secondsForFrames(sequence, audioSettings?.fadeInFrames ?? 0), transitions.incoming
210
+ ? secondsForFrames(sequence, transitions.incoming.durationFrames)
211
+ : 0);
212
+ const fadeOut = Math.max(antiClickFade, secondsForFrames(sequence, audioSettings?.fadeOutFrames ?? 0), transitions.outgoing
213
+ ? secondsForFrames(sequence, transitions.outgoing.durationFrames)
214
+ : 0);
215
+ const chain = [
216
+ ...(timed.reverse ? ["areverse"] : []),
217
+ ...atempoFilters(speed),
218
+ "apad",
219
+ `atrim=duration=${clipDuration}`,
220
+ "asetpts=PTS-STARTPTS",
221
+ `volume=${gain}`,
222
+ ...(fadeIn > 0 ? [`afade=t=in:d=${fadeIn}`] : []),
223
+ ...(fadeOut > 0
224
+ ? [`afade=t=out:st=${Math.max(0, clipDuration - fadeOut)}:d=${fadeOut}`]
225
+ : []),
226
+ `adelay=${Math.round(start * 1000)}|${Math.round(start * 1000)}`,
227
+ ];
228
+ const label = `aclip${index}`;
229
+ filters.push(`[${item.inputIndex}:a]${chain.join(",")}[${label}]`);
230
+ labels.push(`[${label}]`);
231
+ });
232
+ filters.push(`${labels.join("")}amix=inputs=${labels.length}:duration=longest:dropout_transition=0:normalize=0[aout]`);
233
+ return { filters, output: "aout" };
234
+ }
235
+ function buildSequenceFfmpegArgs(sequence, resolved, width, height, outputPath, captionPath) {
236
+ const durationFrames = Math.max(...sequence.clips.map((clip) => clip.timelineStartFrame + clip.durationFrames));
237
+ const duration = secondsForFrames(sequence, durationFrames);
238
+ const video = videoFilter(sequence, resolved, width, height);
239
+ const audio = audioFilter(sequence, resolved, duration);
240
+ const filters = [...video.filters, ...audio.filters];
241
+ let videoOutput = video.output;
242
+ if (captionPath) {
243
+ filters.push(`[${videoOutput}]subtitles='${escapeFilterPath(captionPath)}'[vcaptioned]`);
244
+ videoOutput = "vcaptioned";
245
+ }
246
+ return [
247
+ ...inputArgs(sequence, resolved, width, height),
248
+ "-filter_complex", filters.join(";"),
249
+ "-map", `[${videoOutput}]`,
250
+ ...H264_MEDIUM,
251
+ "-pix_fmt", "yuv420p",
252
+ "-map", `[${audio.output}]`,
253
+ ...AUDIO_OPTS,
254
+ "-t", String(duration),
255
+ "-movflags", "+faststart",
256
+ "-y", outputPath,
257
+ ];
258
+ }
259
+ export function registerExportSequenceJob(engine) {
260
+ registerJobHandler("export_sequence", async (payload, context) => {
261
+ const request = payload;
262
+ const snapshot = engine.sequences.getAtRevision(engine.sequences.getPrimary().sequenceId, request.sequenceRevision);
263
+ if (!snapshot.ok)
264
+ throw new Error(snapshot.error.message);
265
+ const sequence = snapshot.value;
266
+ const [width, height] = sequenceExportDimensions(request.orientation);
267
+ const workspace = await mkdtemp(join(tmpdir(), "videobook-export-"));
268
+ const outputFilename = "original.mp4";
269
+ const outputPath = join(workspace, outputFilename);
270
+ const captionPath = join(workspace, "captions.vtt");
271
+ try {
272
+ const resolved = await resolveClips(engine, sequence, workspace, context.signal);
273
+ if (request.captions !== "omit") {
274
+ await writeFile(captionPath, webVtt(sequence), "utf8");
275
+ }
276
+ await runFfmpeg(buildSequenceFfmpegArgs(sequence, resolved, width, height, outputPath, request.captions === "burn-in" ? captionPath : undefined), { signal: context.signal });
277
+ const label = `final-${request.orientation}-${request.captions}-${request.sequenceRevision.slice(0, 8)}`;
278
+ const existing = engine.artifacts.list().find((artifact) => artifact.kind === "final" && artifact.label === label);
279
+ const final = existing !== undefined
280
+ ? { ok: true, value: existing }
281
+ : await engine.artifacts.create({ kind: "final", label });
282
+ if (!final.ok)
283
+ throw new Error(final.error.message);
284
+ await replaceArtifactFileFromPath(engine, final.value.artifactId, outputFilename, outputPath);
285
+ if (request.captions === "sidecar") {
286
+ await replaceArtifactFileFromPath(engine, final.value.artifactId, "captions.vtt", captionPath);
287
+ }
288
+ const manifest = await engine.files.manifest(final.value.artifactId);
289
+ if (!manifest.ok)
290
+ throw new Error(manifest.error.message);
291
+ const output = manifest.value.files.find((file) => file.name === outputFilename);
292
+ if (!output)
293
+ throw new Error("Rendered MP4 was not persisted");
294
+ const persistedPath = join(manifest.value.path, outputFilename);
295
+ const probe = await probeVideo(persistedPath, { signal: context.signal });
296
+ const registered = engine.streams.list(final.value.artifactId);
297
+ if (!registered.ok)
298
+ throw new Error(registered.error.message);
299
+ if (probe.videoStream
300
+ && !registered.value.some((stream) => stream.kind === "video" && stream.objectHash === output.objectHash)) {
301
+ const stream = await engine.streams.register({
302
+ artifactId: final.value.artifactId,
303
+ sourcePath: outputFilename,
304
+ objectHash: output.objectHash,
305
+ streamIndex: probe.videoStream.index,
306
+ kind: "video",
307
+ timeBase: probe.videoStream.timeBase,
308
+ durationTicks: probe.videoStream.durationTicks,
309
+ codec: probe.videoStream.codec,
310
+ video: probe.videoStream.video,
311
+ });
312
+ if (!stream.ok)
313
+ throw new Error(stream.error.message);
314
+ }
315
+ if (probe.audioStream
316
+ && !registered.value.some((stream) => stream.kind === "audio" && stream.objectHash === output.objectHash)) {
317
+ const stream = await engine.streams.register({
318
+ artifactId: final.value.artifactId,
319
+ sourcePath: outputFilename,
320
+ objectHash: output.objectHash,
321
+ streamIndex: probe.audioStream.index,
322
+ kind: "audio",
323
+ timeBase: probe.audioStream.timeBase,
324
+ durationTicks: probe.audioStream.durationTicks,
325
+ codec: probe.audioStream.codec,
326
+ audio: probe.audioStream.audio,
327
+ });
328
+ if (!stream.ok)
329
+ throw new Error(stream.error.message);
330
+ }
331
+ const provenance = {
332
+ sequenceId: sequence.sequenceId,
333
+ sequenceRevision: request.sequenceRevision,
334
+ renderer: "ffmpeg",
335
+ rendererVersion: "videobook-canonical-v1",
336
+ settings: {
337
+ orientation: request.orientation,
338
+ captions: request.captions,
339
+ width,
340
+ height,
341
+ frameRate: sequence.frameRate,
342
+ videoCodec: "h264",
343
+ audioCodec: "aac",
344
+ },
345
+ inputObjectHashes: [...new Set(resolved.map(({ clip }) => clip.source.kind === "timed" ? clip.source.range.objectHash : clip.source.objectHash))].sort(),
346
+ outputObjectHash: output.objectHash,
347
+ renderedAt: new Date().toISOString(),
348
+ };
349
+ const metadata = await engine.metadata.artifacts.write(final.value.artifactId, "export", provenance);
350
+ if (!metadata.ok)
351
+ throw new Error(metadata.error.message);
352
+ return {
353
+ success: true,
354
+ artifactId: final.value.artifactId,
355
+ label: final.value.label,
356
+ output: outputFilename,
357
+ outputObjectHash: output.objectHash,
358
+ provenance,
359
+ };
360
+ }
361
+ finally {
362
+ await rm(workspace, { recursive: true, force: true });
363
+ }
364
+ });
365
+ }
@@ -0,0 +1,229 @@
1
+ import { createElevenLabs } from "../usage-providers.js";
2
+ import { extname, join } from "node:path";
3
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { audioCrossfadeMs, buildSoundContinuationPrompt, describeSourceAudio, ELEVENLABS_SCRIBE_MODEL, ELEVENLABS_SOUND_EFFECT_MODEL, ELEVENLABS_SOUND_EFFECT_OUTPUT_FORMAT, MAX_SOUND_GENERATION_PROMPT_CHARS, } from "../elevenlabs-audio-extension.js";
6
+ import { extractWaveform, probeVideo, runFfmpeg } from "../ffmpeg.js";
7
+ import { requireApiKey } from "../api-key.js";
8
+ import { registerJobHandler } from "../queue.js";
9
+ const ELEVENLABS_TIMEOUT_MS = 10 * 60_000;
10
+ const DURATION_TOLERANCE_SECONDS = 0.25;
11
+ const TAIL_ACTIVITY_THRESHOLD = 0.005;
12
+ function audioMime(filename) {
13
+ switch (extname(filename).toLowerCase()) {
14
+ case ".mp3": return "audio/mpeg";
15
+ case ".wav": return "audio/wav";
16
+ case ".m4a": return "audio/mp4";
17
+ case ".ogg": return "audio/ogg";
18
+ case ".flac": return "audio/flac";
19
+ default: return "application/octet-stream";
20
+ }
21
+ }
22
+ async function renderContinuation(source, sourceFilename, generated, sourceDurationMs, targetDurationMs, crossfadeMs, signal) {
23
+ const tempDir = await mkdtemp(join(tmpdir(), "videocity-elevenlabs-extend-"));
24
+ const sourcePath = join(tempDir, `source${extname(sourceFilename) || ".audio"}`);
25
+ const generatedPath = join(tempDir, "continuation.mp3");
26
+ const outputPath = join(tempDir, "extended.mp3");
27
+ const sourceSeconds = sourceDurationMs / 1000;
28
+ const targetSeconds = targetDurationMs / 1000;
29
+ const continuationSeconds = (targetDurationMs - sourceDurationMs) / 1000;
30
+ const crossfadeSeconds = crossfadeMs / 1000;
31
+ const generatedSeconds = continuationSeconds + crossfadeSeconds;
32
+ const filter = [
33
+ `[0:a:0]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,atrim=duration=${sourceSeconds},asetpts=PTS-STARTPTS[source]`,
34
+ `[1:a:0]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,atrim=duration=${generatedSeconds},asetpts=PTS-STARTPTS[continuation]`,
35
+ `[source][continuation]acrossfade=d=${crossfadeSeconds}:c1=tri:c2=tri,atrim=duration=${targetSeconds},asetpts=PTS-STARTPTS[out]`,
36
+ ].join(";");
37
+ try {
38
+ await writeFile(sourcePath, source);
39
+ await writeFile(generatedPath, new Uint8Array(generated));
40
+ await runFfmpeg([
41
+ "-y",
42
+ "-i", sourcePath,
43
+ "-i", generatedPath,
44
+ "-filter_complex", filter,
45
+ "-map", "[out]",
46
+ "-c:a", "libmp3lame",
47
+ "-b:a", "192k",
48
+ "-ar", "48000",
49
+ "-ac", "2",
50
+ "-t", String(targetSeconds),
51
+ outputPath,
52
+ ], { signal });
53
+ return await readFile(outputPath);
54
+ }
55
+ finally {
56
+ await rm(tempDir, { recursive: true, force: true, maxRetries: 3 });
57
+ }
58
+ }
59
+ export async function hasAudibleContinuationTail(audioPath) {
60
+ const waveform = await extractWaveform(audioPath, 100);
61
+ if (waveform.length === 0)
62
+ return false;
63
+ const tail = waveform.slice(Math.floor(waveform.length * 0.85));
64
+ const active = tail.filter((level) => level >= TAIL_ACTIVITY_THRESHOLD).length;
65
+ const mean = tail.reduce((sum, level) => sum + level, 0) / tail.length;
66
+ return active / tail.length >= 0.2 && mean >= TAIL_ACTIVITY_THRESHOLD / 2;
67
+ }
68
+ export async function fillLoopableContinuation(audio, durationSeconds, signal) {
69
+ const tempDir = await mkdtemp(join(tmpdir(), "videocity-elevenlabs-loop-fill-"));
70
+ const inputPath = join(tempDir, "input.mp3");
71
+ const trimmedPath = join(tempDir, "trimmed.wav");
72
+ const outputPath = join(tempDir, "filled.mp3");
73
+ try {
74
+ await writeFile(inputPath, new Uint8Array(audio));
75
+ await runFfmpeg([
76
+ "-y",
77
+ "-i", inputPath,
78
+ "-af", "areverse,silenceremove=start_periods=1:start_duration=0.15:start_threshold=-45dB,areverse",
79
+ "-c:a", "pcm_s16le",
80
+ "-ar", "48000",
81
+ "-ac", "2",
82
+ trimmedPath,
83
+ ], { signal });
84
+ const trimmedProbe = await probeVideo(trimmedPath, { signal });
85
+ const trimmedDuration = trimmedProbe.audioDuration ?? trimmedProbe.duration;
86
+ if (!Number.isFinite(trimmedDuration) || trimmedDuration < 0.5) {
87
+ throw new Error("ElevenLabs loopable continuation has too little audible content");
88
+ }
89
+ const crossfadeSeconds = Math.min(0.15, trimmedDuration / 4);
90
+ const repeatCount = Math.max(1, Math.ceil((durationSeconds - crossfadeSeconds) / (trimmedDuration - crossfadeSeconds)));
91
+ const args = ["-y"];
92
+ for (let index = 0; index < repeatCount; index++) {
93
+ args.push("-i", trimmedPath);
94
+ }
95
+ const filters = [];
96
+ if (repeatCount === 1) {
97
+ filters.push(`[0:a:0]atrim=duration=${durationSeconds},asetpts=PTS-STARTPTS[out]`);
98
+ }
99
+ else {
100
+ let previous = "[0:a:0]";
101
+ for (let index = 1; index < repeatCount; index++) {
102
+ const output = index === repeatCount - 1 ? "[joined]" : `[join${index}]`;
103
+ filters.push(`${previous}[${index}:a:0]acrossfade=d=${crossfadeSeconds}:c1=tri:c2=tri${output}`);
104
+ previous = output;
105
+ }
106
+ filters.push(`[joined]atrim=duration=${durationSeconds},asetpts=PTS-STARTPTS[out]`);
107
+ }
108
+ args.push("-filter_complex", filters.join(";"), "-map", "[out]", "-c:a", "libmp3lame", "-b:a", "192k", "-ar", "48000", "-ac", "2", "-t", String(durationSeconds), outputPath);
109
+ await runFfmpeg(args, { signal });
110
+ if (!(await hasAudibleContinuationTail(outputPath))) {
111
+ throw new Error("ElevenLabs loop-filled continuation still has a silent tail");
112
+ }
113
+ return Uint8Array.from(await readFile(outputPath)).buffer;
114
+ }
115
+ finally {
116
+ await rm(tempDir, { recursive: true, force: true, maxRetries: 3 });
117
+ }
118
+ }
119
+ async function generateAudibleContinuation(provider, generationPrompt, generationDurationSeconds, signal) {
120
+ const retrySuffix = " Audible every moment; seamless loop; no quiet tail.";
121
+ const attempts = [
122
+ { text: generationPrompt, loop: false },
123
+ {
124
+ text: `${generationPrompt.slice(0, MAX_SOUND_GENERATION_PROMPT_CHARS - retrySuffix.length)}${retrySuffix}`,
125
+ loop: true,
126
+ },
127
+ ];
128
+ for (let index = 0; index < attempts.length; index++) {
129
+ const attempt = attempts[index];
130
+ const audio = await provider.v1.soundGeneration({
131
+ text: attempt.text,
132
+ model_id: ELEVENLABS_SOUND_EFFECT_MODEL,
133
+ duration_seconds: generationDurationSeconds,
134
+ prompt_influence: 0.9,
135
+ loop: attempt.loop,
136
+ output_format: ELEVENLABS_SOUND_EFFECT_OUTPUT_FORMAT,
137
+ }, signal);
138
+ const tempDir = await mkdtemp(join(tmpdir(), "videocity-elevenlabs-tail-check-"));
139
+ const audioPath = join(tempDir, "continuation.mp3");
140
+ try {
141
+ await writeFile(audioPath, new Uint8Array(audio));
142
+ if (await hasAudibleContinuationTail(audioPath)) {
143
+ return { audio, attempts: index + 1, loopFilled: false };
144
+ }
145
+ }
146
+ finally {
147
+ await rm(tempDir, { recursive: true, force: true, maxRetries: 3 });
148
+ }
149
+ if (attempt.loop) {
150
+ const filled = await fillLoopableContinuation(audio, generationDurationSeconds, signal);
151
+ return {
152
+ audio: filled,
153
+ attempts: index + 1,
154
+ loopFilled: true,
155
+ };
156
+ }
157
+ }
158
+ throw new Error("ElevenLabs generated an unusable continuation twice");
159
+ }
160
+ export async function extendAudioWithElevenLabs(engine, payload, provider, signal) {
161
+ const source = await engine.files.read(payload.sourceAssetId, payload.sourceFilename);
162
+ if (!source.ok)
163
+ throw new Error(`Failed to read source audio: ${source.error.message}`);
164
+ const sourceBytes = new Uint8Array(source.value);
165
+ const transcript = await provider.v1.speechToText({
166
+ model_id: ELEVENLABS_SCRIBE_MODEL,
167
+ file: new Blob([sourceBytes], { type: audioMime(payload.sourceFilename) }),
168
+ tag_audio_events: true,
169
+ diarize: false,
170
+ }, signal);
171
+ const sourceDescription = describeSourceAudio(transcript);
172
+ const continuationDurationMs = payload.targetDurationMs - payload.sourceDurationMs;
173
+ const crossfadeMs = audioCrossfadeMs(payload.sourceDurationMs, continuationDurationMs);
174
+ const generationDurationSeconds = (continuationDurationMs + crossfadeMs) / 1000;
175
+ const generationPrompt = buildSoundContinuationPrompt(sourceDescription, generationDurationSeconds, payload.continuationPrompt);
176
+ const generated = await generateAudibleContinuation(provider, generationPrompt, generationDurationSeconds, signal);
177
+ const output = await renderContinuation(source.value, payload.sourceFilename, generated.audio, payload.sourceDurationMs, payload.targetDurationMs, crossfadeMs, signal);
178
+ const write = await engine.files.write(payload.audAssetId, "original.mp3", output);
179
+ if (!write.ok)
180
+ throw new Error(`Failed to write extended audio: ${write.error.message}`);
181
+ const assetDir = await engine.workspaces.resolveArtifact(payload.audAssetId);
182
+ if (!assetDir.ok)
183
+ throw new Error(`Failed to resolve extended audio: ${assetDir.error.message}`);
184
+ const probe = await probeVideo(join(assetDir.value, "original.mp3"));
185
+ const duration = probe.audioDuration ?? probe.duration;
186
+ const targetSeconds = payload.targetDurationMs / 1000;
187
+ if (!Number.isFinite(duration) || Math.abs(duration - targetSeconds) > DURATION_TOLERANCE_SECONDS) {
188
+ throw new Error(`Extended audio is ${duration.toFixed(3)}s, expected ${targetSeconds.toFixed(3)}s`);
189
+ }
190
+ const metadata = await engine.metadata.artifacts.write(payload.audAssetId, "original", {
191
+ origin: "elevenlabs_sound_generation_extend",
192
+ provider: "elevenlabs",
193
+ transcription_model: ELEVENLABS_SCRIBE_MODEL,
194
+ generation_model: ELEVENLABS_SOUND_EFFECT_MODEL,
195
+ output_format: ELEVENLABS_SOUND_EFFECT_OUTPUT_FORMAT,
196
+ source_asset: payload.sourceAssetId,
197
+ source_description: sourceDescription,
198
+ source_duration: payload.sourceDurationMs / 1000,
199
+ target_duration: targetSeconds,
200
+ continuation_duration: continuationDurationMs / 1000,
201
+ generation_duration: generationDurationSeconds,
202
+ generation_attempts: generated.attempts,
203
+ generation_loop_filled: generated.loopFilled,
204
+ crossfade_duration: crossfadeMs / 1000,
205
+ continuation_prompt: payload.continuationPrompt ?? null,
206
+ duration,
207
+ generated_at: new Date().toISOString(),
208
+ });
209
+ if (!metadata.ok)
210
+ throw new Error(`Failed to write extended audio metadata: ${metadata.error.message}`);
211
+ return {
212
+ duration,
213
+ sourceDescription,
214
+ generationAttempts: generated.attempts,
215
+ loopFilled: generated.loopFilled,
216
+ };
217
+ }
218
+ export function registerExtendAudioJob(engine) {
219
+ registerJobHandler("extend_audio", async (rawPayload, ctx) => {
220
+ const payload = rawPayload;
221
+ ctx.getOwnerId();
222
+ const provider = createElevenLabs({
223
+ apiKey: requireApiKey("ELEVENLABS_API_KEY"),
224
+ timeout: ELEVENLABS_TIMEOUT_MS,
225
+ });
226
+ const result = await extendAudioWithElevenLabs(engine, payload, provider, ctx.signal);
227
+ return { success: true, provider: "elevenlabs", ...result };
228
+ });
229
+ }
@@ -0,0 +1,72 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { registerJobHandler } from "../queue.js";
5
+ import { withLockedLocalLease } from "./_lease.js";
6
+ import { extractWaveform, probeVideo, runFfmpeg } from "../ffmpeg.js";
7
+ import { validateMediaOutputPaths, withMediaOutputLock } from "../media-output-path.js";
8
+ import { registerMediaStreams } from "./process-video.js";
9
+ const LOCK_DURATION_MS = 120_000;
10
+ export function registerExtractAudioJob(engine) {
11
+ registerJobHandler("extract_audio", async (payload, ctx) => {
12
+ const { inputPath, audioAssetId, audioAssetDir, sourceVideoId, destination, range, } = payload;
13
+ const destinationMode = destination !== undefined;
14
+ const extract = async () => {
15
+ const outputFilename = destination
16
+ ? (await validateMediaOutputPaths(engine, audioAssetId, [destination.filename], ".mp3"))[0]
17
+ : "original.mp3";
18
+ const workspace = await mkdtemp(join(tmpdir(), "videobook-extract-audio-"));
19
+ const outputPath = join(workspace, "output.mp3");
20
+ try {
21
+ const ffmpegArgs = [
22
+ "-y",
23
+ "-i", inputPath,
24
+ "-vn",
25
+ "-acodec", "libmp3lame",
26
+ "-q:a", "2",
27
+ ...(range ? ["-ss", String(range.start_s), "-t", String(range.duration_s)] : []),
28
+ outputPath,
29
+ ];
30
+ await runFfmpeg(ffmpegArgs, { signal: ctx.signal });
31
+ const probe = await probeVideo(outputPath, { signal: ctx.signal });
32
+ const duration = probe.audioDuration ?? probe.duration;
33
+ if (!probe.hasAudio || !Number.isFinite(duration) || duration <= 0) {
34
+ throw new Error("Audio extraction produced no playable audio");
35
+ }
36
+ const written = await engine.files.writeFromPath(audioAssetId, outputFilename, outputPath);
37
+ if (!written.ok)
38
+ throw new Error(written.error.message);
39
+ const recorded = await engine.history.recordOperation("extract_audio", audioAssetId, {
40
+ source_asset: sourceVideoId,
41
+ filename: outputFilename,
42
+ ...(range ? { range } : {}),
43
+ duration,
44
+ });
45
+ if (!recorded.ok)
46
+ throw new Error(recorded.error.message);
47
+ if (!destinationMode) {
48
+ const origin = {
49
+ origin: "extract_audio",
50
+ source_asset: sourceVideoId,
51
+ duration,
52
+ };
53
+ await engine.metadata.artifacts.write(audioAssetId, "original", origin);
54
+ await registerMediaStreams(engine, audioAssetId, outputFilename, probe);
55
+ const peaks = await extractWaveform(outputPath);
56
+ if (peaks.length > 0) {
57
+ const waveform = await engine.metadata.waveforms.write(audioAssetId, peaks);
58
+ if (!waveform.ok)
59
+ throw new Error(waveform.error.message);
60
+ }
61
+ }
62
+ return { success: true };
63
+ }
64
+ finally {
65
+ await rm(workspace, { recursive: true, force: true });
66
+ }
67
+ };
68
+ if (destinationMode)
69
+ return withMediaOutputLock(engine, audioAssetDir, ctx.signal, extract);
70
+ return withLockedLocalLease(engine, audioAssetId, audioAssetDir, ctx, "extract", { durationMs: LOCK_DURATION_MS }, extract);
71
+ });
72
+ }