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,257 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { AUDIO_OPTS, FFMPEG, H264_MEDIUM, probeVideo, runFfmpeg, } from "../ffmpeg.js";
4
+ import { registerJobHandler, queue } from "../queue.js";
5
+ import { withLockedLocalLease } from "./_lease.js";
6
+ import { ingestWorkspaceOutputs } from "./_persist-output.js";
7
+ import { fixedTransitionSeconds, MIN_TRANSITION_SECONDS } from "../scene-transition.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const LOCK_DURATION_MS = 1_800_000;
10
+ const PCM_SAMPLE_RATE = 8_000;
11
+ const MIN_AUDIO_OVERLAP_SECONDS = 0.08;
12
+ const MAX_AUDIO_OVERLAP_SECONDS = 3;
13
+ const AUDIO_MATCH_THRESHOLD = 0.8;
14
+ function mediaNumber(value) {
15
+ return value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
16
+ }
17
+ async function decodeMonoPcm(path, startSeconds, durationSeconds, signal) {
18
+ const { stdout } = await execFileAsync(FFMPEG, [
19
+ "-hide_banner",
20
+ "-loglevel", "error",
21
+ "-i", path,
22
+ "-ss", mediaNumber(startSeconds),
23
+ "-t", mediaNumber(durationSeconds),
24
+ "-map", "0:a:0",
25
+ "-vn",
26
+ "-ac", "1",
27
+ "-ar", String(PCM_SAMPLE_RATE),
28
+ "-f", "s16le",
29
+ "pipe:1",
30
+ ], {
31
+ encoding: "buffer",
32
+ maxBuffer: 2 * 1024 * 1024,
33
+ signal,
34
+ });
35
+ const buffer = stdout;
36
+ return new Int16Array(buffer.buffer, buffer.byteOffset, Math.floor(buffer.byteLength / Int16Array.BYTES_PER_ELEMENT));
37
+ }
38
+ function correlationForOverlap(first, second, overlapSamples) {
39
+ const firstStart = first.length - overlapSamples;
40
+ let firstSum = 0;
41
+ let secondSum = 0;
42
+ let firstSquares = 0;
43
+ let secondSquares = 0;
44
+ let products = 0;
45
+ for (let index = 0; index < overlapSamples; index++) {
46
+ const firstSample = first[firstStart + index];
47
+ const secondSample = second[index];
48
+ firstSum += firstSample;
49
+ secondSum += secondSample;
50
+ firstSquares += firstSample * firstSample;
51
+ secondSquares += secondSample * secondSample;
52
+ products += firstSample * secondSample;
53
+ }
54
+ const firstVariance = firstSquares - (firstSum * firstSum) / overlapSamples;
55
+ const secondVariance = secondSquares - (secondSum * secondSum) / overlapSamples;
56
+ const minimumVariance = overlapSamples * 64 * 64;
57
+ if (firstVariance < minimumVariance || secondVariance < minimumVariance)
58
+ return null;
59
+ const covariance = products - (firstSum * secondSum) / overlapSamples;
60
+ return covariance / Math.sqrt(firstVariance * secondVariance);
61
+ }
62
+ function bestAudioOverlap(first, second) {
63
+ const minimumSamples = Math.ceil(MIN_AUDIO_OVERLAP_SECONDS * PCM_SAMPLE_RATE);
64
+ const maximumSamples = Math.min(first.length, second.length);
65
+ if (maximumSamples < minimumSamples)
66
+ return null;
67
+ const coarseStep = Math.max(1, Math.round(PCM_SAMPLE_RATE / 1_000));
68
+ let bestSamples = minimumSamples;
69
+ let bestScore = Number.NEGATIVE_INFINITY;
70
+ const evaluate = (samples) => {
71
+ const score = correlationForOverlap(first, second, samples);
72
+ if (score === null)
73
+ return;
74
+ if (score > bestScore || (score === bestScore && samples > bestSamples)) {
75
+ bestSamples = samples;
76
+ bestScore = score;
77
+ }
78
+ };
79
+ for (let samples = minimumSamples; samples <= maximumSamples; samples += coarseStep) {
80
+ evaluate(samples);
81
+ }
82
+ evaluate(maximumSamples);
83
+ const refineStart = Math.max(minimumSamples, bestSamples - coarseStep + 1);
84
+ const refineEnd = Math.min(maximumSamples, bestSamples + coarseStep - 1);
85
+ for (let samples = refineStart; samples <= refineEnd; samples++)
86
+ evaluate(samples);
87
+ if (!Number.isFinite(bestScore) || bestScore < AUDIO_MATCH_THRESHOLD)
88
+ return null;
89
+ return {
90
+ overlapSeconds: bestSamples / PCM_SAMPLE_RATE,
91
+ score: bestScore,
92
+ };
93
+ }
94
+ async function detectSharedAudioOverlap(firstPath, secondPath, firstAudioDuration, secondAudioDuration, signal) {
95
+ const windowSeconds = Math.min(MAX_AUDIO_OVERLAP_SECONDS, firstAudioDuration - MIN_TRANSITION_SECONDS, secondAudioDuration - MIN_TRANSITION_SECONDS);
96
+ if (windowSeconds < MIN_AUDIO_OVERLAP_SECONDS)
97
+ return null;
98
+ const [first, second] = await Promise.all([
99
+ decodeMonoPcm(firstPath, Math.max(0, firstAudioDuration - windowSeconds), windowSeconds, signal),
100
+ decodeMonoPcm(secondPath, 0, windowSeconds, signal),
101
+ ]);
102
+ return bestAudioOverlap(first, second);
103
+ }
104
+ async function resolveTransitionTiming(firstPath, secondPath, first, second, requested, signal) {
105
+ if (requested === undefined &&
106
+ first.hasAudio &&
107
+ second.hasAudio &&
108
+ first.audioDuration !== null &&
109
+ second.audioDuration !== null) {
110
+ const match = await detectSharedAudioOverlap(firstPath, secondPath, first.audioDuration, second.audioDuration, signal);
111
+ if (match) {
112
+ const secondStartSeconds = first.audioDuration - match.overlapSeconds;
113
+ const transitionSeconds = first.videoDuration - secondStartSeconds;
114
+ if (secondStartSeconds >= 0 &&
115
+ transitionSeconds >= MIN_TRANSITION_SECONDS &&
116
+ transitionSeconds < first.videoDuration &&
117
+ transitionSeconds < second.videoDuration) {
118
+ return {
119
+ mode: "audio_match",
120
+ transitionSeconds,
121
+ secondStartSeconds,
122
+ audioOverlapSeconds: match.overlapSeconds,
123
+ matchScore: match.score,
124
+ };
125
+ }
126
+ }
127
+ }
128
+ const transitionSeconds = fixedTransitionSeconds(first.videoDuration, second.videoDuration, requested);
129
+ return {
130
+ mode: "fixed",
131
+ transitionSeconds,
132
+ secondStartSeconds: first.videoDuration - transitionSeconds,
133
+ audioOverlapSeconds: transitionSeconds,
134
+ matchScore: null,
135
+ };
136
+ }
137
+ function normalizedVideoFilter(input, label, width, height, fps) {
138
+ // Keep fps last: some Linux FFmpeg builds report a 1/0 frame rate after
139
+ // setpts, which xfade correctly rejects as variable-frame-rate input.
140
+ return `[${input}:v]scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1,format=yuv420p,settb=AVTB,setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=${mediaNumber(1 / fps)},fps=${mediaNumber(fps)}[${label}]`;
141
+ }
142
+ function fixedAudioFilter(input, label, duration, hasAudio) {
143
+ if (!hasAudio) {
144
+ return `anullsrc=r=48000:cl=stereo:d=${mediaNumber(duration)}[${label}]`;
145
+ }
146
+ return `[${input}:a]aresample=48000,aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo,apad,atrim=duration=${mediaNumber(duration)},asetpts=PTS-STARTPTS[${label}]`;
147
+ }
148
+ function matchedAudioFilter(input, label, duration) {
149
+ return `[${input}:a]aresample=48000,aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo,atrim=duration=${mediaNumber(duration)},asetpts=PTS-STARTPTS[${label}]`;
150
+ }
151
+ async function renderSmoothConcat(options, first, second, timing) {
152
+ const width = first.width;
153
+ const height = first.height;
154
+ const fps = first.fps;
155
+ const outputDuration = timing.secondStartSeconds + second.videoDuration;
156
+ const filters = [
157
+ normalizedVideoFilter(0, "v0", width, height, fps),
158
+ normalizedVideoFilter(1, "v1", width, height, fps),
159
+ `[v0][v1]xfade=transition=fade:duration=${mediaNumber(timing.transitionSeconds)}:offset=${mediaNumber(timing.secondStartSeconds)},trim=duration=${mediaNumber(outputDuration)},format=yuv420p[v]`,
160
+ ];
161
+ const withAudio = first.hasAudio || second.hasAudio;
162
+ if (withAudio) {
163
+ if (timing.mode === "audio_match" &&
164
+ first.audioDuration !== null &&
165
+ second.audioDuration !== null) {
166
+ filters.push(matchedAudioFilter(0, "a0", first.audioDuration));
167
+ filters.push(matchedAudioFilter(1, "a1", second.audioDuration));
168
+ }
169
+ else {
170
+ filters.push(fixedAudioFilter(0, "a0", first.videoDuration, first.hasAudio));
171
+ filters.push(fixedAudioFilter(1, "a1", second.videoDuration, second.hasAudio));
172
+ }
173
+ filters.push(`[a0][a1]acrossfade=d=${mediaNumber(timing.audioOverlapSeconds)}:c1=tri:c2=tri,apad,atrim=duration=${mediaNumber(outputDuration)}[a]`);
174
+ }
175
+ await runFfmpeg([
176
+ "-y",
177
+ "-i", options.firstPath,
178
+ "-i", options.secondPath,
179
+ "-filter_complex", filters.join(";"),
180
+ "-map", "[v]",
181
+ ...(withAudio ? ["-map", "[a]"] : []),
182
+ ...H264_MEDIUM,
183
+ ...(withAudio ? AUDIO_OPTS : []),
184
+ "-movflags", "+faststart",
185
+ options.outputPath,
186
+ ], { signal: options.signal });
187
+ return outputDuration;
188
+ }
189
+ function validateProbe(label, probe) {
190
+ if (probe.width <= 0 || probe.height <= 0 || probe.videoDuration <= 0 || probe.fps <= 0) {
191
+ throw new Error(`${label} input is not a valid video`);
192
+ }
193
+ }
194
+ export async function smoothSceneConcatFiles(options) {
195
+ const [first, second] = await Promise.all([
196
+ probeVideo(options.firstPath, { signal: options.signal }),
197
+ probeVideo(options.secondPath, { signal: options.signal }),
198
+ ]);
199
+ validateProbe("First", first);
200
+ validateProbe("Second", second);
201
+ const timing = await resolveTransitionTiming(options.firstPath, options.secondPath, first, second, options.transitionSeconds, options.signal);
202
+ const outputDuration = await renderSmoothConcat(options, first, second, timing);
203
+ const output = await probeVideo(options.outputPath, { signal: options.signal });
204
+ validateProbe("Output", output);
205
+ if (Math.abs(output.videoDuration - outputDuration) > 1 / first.fps + 0.01) {
206
+ throw new Error(`Scene concat produced ${output.videoDuration.toFixed(3)}s of video instead of ${outputDuration.toFixed(3)}s`);
207
+ }
208
+ if ((first.hasAudio || second.hasAudio) && (!output.hasAudio
209
+ || Math.abs((output.audioDuration ?? output.duration) - outputDuration) > 0.15)) {
210
+ throw new Error("Scene concat output has missing or incomplete audio");
211
+ }
212
+ return { ...timing, outputDuration };
213
+ }
214
+ export function registerSmoothSceneConcatJob(engine) {
215
+ registerJobHandler("smooth_scene_concat", async (payload, ctx) => {
216
+ const { firstPath, secondPath, outputPath, firstAssetId, secondAssetId, assetId, assetDir, bookId, transitionSeconds, } = payload;
217
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "splice", { durationMs: LOCK_DURATION_MS, data: { state: "splicing" } }, async () => {
218
+ const concat = await smoothSceneConcatFiles({
219
+ firstPath,
220
+ secondPath,
221
+ outputPath,
222
+ transitionSeconds,
223
+ signal: ctx.signal,
224
+ });
225
+ const provenance = {
226
+ origin: "smooth_scene_concat",
227
+ first_asset: firstAssetId,
228
+ second_asset: secondAssetId,
229
+ alignment_mode: concat.mode,
230
+ transition_seconds: concat.transitionSeconds,
231
+ second_start_seconds: concat.secondStartSeconds,
232
+ audio_overlap_seconds: concat.audioOverlapSeconds,
233
+ match_score: concat.matchScore,
234
+ output_duration: concat.outputDuration,
235
+ concatenated_at: new Date().toISOString(),
236
+ };
237
+ await ingestWorkspaceOutputs(engine, assetId, [outputPath.split("/").pop()], "smooth_scene_concat", provenance);
238
+ await engine.metadata.artifacts.write(assetId, "original", provenance);
239
+ return { concat, provenance };
240
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
241
+ const processJobId = `process-video-${assetId}-${Date.now()}`;
242
+ await queue.enqueue(processJobId, {
243
+ type: "process_video",
244
+ videoPath: outputPath,
245
+ assetId,
246
+ assetDir,
247
+ bookId,
248
+ provenance: result.provenance,
249
+ });
250
+ return {
251
+ success: true,
252
+ alignment_mode: result.concat.mode,
253
+ transition_seconds: result.concat.transitionSeconds,
254
+ match_score: result.concat.matchScore,
255
+ };
256
+ });
257
+ }
@@ -0,0 +1,280 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, extname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { registerJobHandler, queue } from "../queue.js";
7
+ import { runFfmpeg, FFMPEG, H264_MEDIUM, AUDIO_OPTS, probeVideo, withQuietFfmpegArgs } from "../ffmpeg.js";
8
+ import { withLockedLocalLease } from "./_lease.js";
9
+ import { ingestWorkspaceOutputs } from "./_persist-output.js";
10
+ const execFileAsync = promisify(execFile);
11
+ const LOCK_DURATION_MS = 1_800_000;
12
+ // Downsampled grayscale resolution used for both the target reference frame
13
+ // and the scanned frames in video1. Small enough to keep the inner MAE loop
14
+ // cheap (~4k bytes/frame) and to bound memory; large enough to discriminate
15
+ // between visually distinct frames.
16
+ const MATCH_W = 64;
17
+ const MATCH_H = 64;
18
+ const MATCH_FRAME_BYTES = MATCH_W * MATCH_H;
19
+ async function extractTargetFrame(videoPath) {
20
+ const { stdout } = await execFileAsync(FFMPEG, withQuietFfmpegArgs([
21
+ "-y",
22
+ "-i", videoPath,
23
+ "-vf", `scale=${MATCH_W}:${MATCH_H},format=gray`,
24
+ "-frames:v", "1",
25
+ "-an",
26
+ "-f", "rawvideo",
27
+ "-pix_fmt", "gray",
28
+ "pipe:1",
29
+ ]), { encoding: "buffer", maxBuffer: 1024 * 1024 });
30
+ const buf = stdout;
31
+ if (buf.length < MATCH_FRAME_BYTES) {
32
+ throw new Error(`Target frame extraction returned ${buf.length} bytes, expected ${MATCH_FRAME_BYTES}`);
33
+ }
34
+ return buf.subarray(0, MATCH_FRAME_BYTES);
35
+ }
36
+ async function findBestMatch(videoPath, target, fps, startFrame) {
37
+ const startTime = startFrame / fps;
38
+ const proc = spawn(FFMPEG, withQuietFfmpegArgs([
39
+ ...(startFrame > 0 ? ["-ss", String(startTime)] : []),
40
+ "-i", videoPath,
41
+ "-vf", `fps=${fps},scale=${MATCH_W}:${MATCH_H},format=gray`,
42
+ "-an",
43
+ "-f", "rawvideo",
44
+ "-pix_fmt", "gray",
45
+ "pipe:1",
46
+ ]), { stdio: ["ignore", "pipe", "ignore"] });
47
+ let buffer = Buffer.alloc(0);
48
+ let frameIndex = 0;
49
+ let bestScore = Number.POSITIVE_INFINITY;
50
+ let bestIndex = -1;
51
+ for await (const chunk of proc.stdout) {
52
+ const incoming = chunk;
53
+ buffer = buffer.length === 0 ? incoming : Buffer.concat([buffer, incoming]);
54
+ while (buffer.length >= MATCH_FRAME_BYTES) {
55
+ let score = 0;
56
+ for (let i = 0; i < MATCH_FRAME_BYTES; i++) {
57
+ const d = buffer[i] - target[i];
58
+ score += d < 0 ? -d : d;
59
+ }
60
+ if (score < bestScore) {
61
+ bestScore = score;
62
+ bestIndex = frameIndex;
63
+ }
64
+ buffer = buffer.subarray(MATCH_FRAME_BYTES);
65
+ frameIndex++;
66
+ }
67
+ }
68
+ await new Promise((resolve, reject) => {
69
+ proc.on("close", (code) => {
70
+ if (code === 0 || code === null)
71
+ resolve();
72
+ else
73
+ reject(new Error(`ffmpeg search exited with code ${code}`));
74
+ });
75
+ proc.on("error", reject);
76
+ });
77
+ if (bestIndex < 0)
78
+ throw new Error(`No frames decoded from ${videoPath}`);
79
+ return {
80
+ frameIndex: startFrame + bestIndex,
81
+ timestamp: startTime + bestIndex / fps,
82
+ score: bestScore / MATCH_FRAME_BYTES,
83
+ };
84
+ }
85
+ async function spliceConcat(firstPath, secondPath, splicePoint, outputPath, width, height, fps, withAudio) {
86
+ const scalePad = `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
87
+ const firstVideo = splicePoint === null
88
+ ? `[0:v]fps=${fps},${scalePad},setpts=PTS-STARTPTS[v0]`
89
+ : `[0:v]trim=end=${splicePoint},fps=${fps},${scalePad},setpts=PTS-STARTPTS[v0]`;
90
+ const filterParts = [
91
+ firstVideo,
92
+ `[1:v]fps=${fps},${scalePad},setpts=PTS-STARTPTS[v1]`,
93
+ ];
94
+ if (withAudio) {
95
+ const firstAudio = splicePoint === null
96
+ ? `[0:a]asetpts=PTS-STARTPTS[a0]`
97
+ : `[0:a]atrim=end=${splicePoint},asetpts=PTS-STARTPTS[a0]`;
98
+ filterParts.push(firstAudio);
99
+ filterParts.push(`[1:a]asetpts=PTS-STARTPTS[a1]`);
100
+ filterParts.push(`[v0][a0][v1][a1]concat=n=2:v=1:a=1[v][a]`);
101
+ }
102
+ else {
103
+ filterParts.push(`[v0][v1]concat=n=2:v=1:a=0[v]`);
104
+ }
105
+ const args = [
106
+ "-y",
107
+ "-i", firstPath,
108
+ "-i", secondPath,
109
+ "-filter_complex", filterParts.join(";"),
110
+ "-map", "[v]",
111
+ ...(withAudio ? ["-map", "[a]"] : []),
112
+ ...H264_MEDIUM,
113
+ ...(withAudio ? AUDIO_OPTS : []),
114
+ outputPath,
115
+ ];
116
+ await runFfmpeg(args);
117
+ }
118
+ async function verifySpliceDuration(outputPath, expectedDuration, fps) {
119
+ const output = await probeVideo(outputPath);
120
+ const tolerance = Math.max(0.25, 2 / fps);
121
+ if (Math.abs(output.videoDuration - expectedDuration) <= tolerance)
122
+ return;
123
+ throw new Error(`Splice output duration ${output.videoDuration.toFixed(3)}s does not match expected ${expectedDuration.toFixed(3)}s`);
124
+ }
125
+ async function readArtifactFile(engine, artifactId, filename) {
126
+ const result = await engine.files.read(artifactId, filename);
127
+ if (!result.ok) {
128
+ throw new Error(`Failed to snapshot ${artifactId}/${filename}: ${result.error.message}`);
129
+ }
130
+ return result.value;
131
+ }
132
+ export async function spliceVideoChain(options) {
133
+ const { clips, outputPath, workDir, width, height, fps, overlap } = options;
134
+ if (clips.length < 2) {
135
+ throw new Error("spliceVideoChain requires at least two clips");
136
+ }
137
+ let current = clips[0];
138
+ let lastResult = null;
139
+ for (let index = 1; index < clips.length; index += 1) {
140
+ const next = clips[index];
141
+ const isLast = index === clips.length - 1;
142
+ const dest = isLast ? outputPath : join(workDir, `acc-${index}.mp4`);
143
+ lastResult = await spliceVideoFiles({
144
+ firstPath: current.path,
145
+ secondPath: next.path,
146
+ outputPath: dest,
147
+ width,
148
+ height,
149
+ fps,
150
+ firstDuration: current.duration,
151
+ secondDuration: next.duration,
152
+ withAudio: current.hasAudio && next.hasAudio,
153
+ overlap,
154
+ });
155
+ current = {
156
+ path: dest,
157
+ duration: lastResult.timestamp + next.duration,
158
+ hasAudio: current.hasAudio && next.hasAudio,
159
+ };
160
+ }
161
+ if (!lastResult)
162
+ throw new Error("spliceVideoChain produced no output");
163
+ return lastResult;
164
+ }
165
+ export async function spliceVideoFiles(options) {
166
+ const { firstPath, secondPath, outputPath, width, height, fps, firstDuration, secondDuration, withAudio, overlap, } = options;
167
+ if (!Number.isFinite(firstDuration) || firstDuration <= 0) {
168
+ throw new Error(`Invalid first video duration: ${firstDuration}`);
169
+ }
170
+ if (!Number.isFinite(fps) || fps <= 0) {
171
+ throw new Error(`Invalid first video frame rate: ${fps}`);
172
+ }
173
+ if (!Number.isFinite(secondDuration) || secondDuration <= 0) {
174
+ throw new Error(`Invalid second video duration: ${secondDuration}`);
175
+ }
176
+ if (!Number.isFinite(overlap) || overlap < 0) {
177
+ throw new Error(`Overlap must be a non-negative number, got: ${overlap}`);
178
+ }
179
+ if (overlap === 0) {
180
+ await spliceConcat(firstPath, secondPath, null, outputPath, width, height, fps, withAudio);
181
+ await verifySpliceDuration(outputPath, firstDuration + secondDuration, fps);
182
+ return { timestamp: firstDuration, frameIndex: null, score: null };
183
+ }
184
+ const minimumWindow = 1 / fps;
185
+ const searchStart = Math.max(0, firstDuration - Math.max(overlap, minimumWindow));
186
+ const startFrame = Math.floor(searchStart * fps);
187
+ const target = await extractTargetFrame(secondPath);
188
+ const match = await findBestMatch(firstPath, target, fps, startFrame);
189
+ await spliceConcat(firstPath, secondPath, match.timestamp, outputPath, width, height, fps, withAudio);
190
+ await verifySpliceDuration(outputPath, match.timestamp + secondDuration, fps);
191
+ return match;
192
+ }
193
+ function clipPayloads(payload) {
194
+ if (Array.isArray(payload.clips) && payload.clips.length >= 2) {
195
+ return payload.clips;
196
+ }
197
+ return [
198
+ {
199
+ assetId: payload.firstAssetId,
200
+ filename: payload.firstFilename ?? basename(payload.firstPath),
201
+ duration: payload.firstDuration,
202
+ hasAudio: payload.firstHasAudio,
203
+ },
204
+ {
205
+ assetId: payload.secondAssetId,
206
+ filename: payload.secondFilename ?? basename(payload.secondPath),
207
+ duration: payload.secondDuration,
208
+ hasAudio: payload.secondHasAudio,
209
+ },
210
+ ];
211
+ }
212
+ export function registerSpliceVideoJob(engine) {
213
+ registerJobHandler("splice_video", async (payload, ctx) => {
214
+ const typedPayload = payload;
215
+ const { outputPath, assetId, assetDir, bookId, targetWidth, targetHeight, firstFps, overlap, } = typedPayload;
216
+ const clips = clipPayloads(typedPayload);
217
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "splice", { durationMs: LOCK_DURATION_MS, data: { state: "splicing" } }, async () => {
218
+ const sourceDir = await mkdtemp(join(tmpdir(), "videobook-splice-source-"));
219
+ try {
220
+ const snapshots = await Promise.all(clips.map(async (clip, index) => {
221
+ const snapshotPath = join(sourceDir, `${index}${extname(clip.filename)}`);
222
+ const bytes = await readArtifactFile(engine, clip.assetId, clip.filename);
223
+ await writeFile(snapshotPath, bytes);
224
+ const probed = typeof clip.duration === "number"
225
+ ? null
226
+ : await probeVideo(snapshotPath);
227
+ return {
228
+ path: snapshotPath,
229
+ duration: typeof clip.duration === "number"
230
+ ? clip.duration
231
+ : probed.videoDuration,
232
+ hasAudio: clip.hasAudio,
233
+ };
234
+ }));
235
+ const resolvedOverlap = typeof overlap === "number"
236
+ ? overlap
237
+ : snapshots.length > 2
238
+ ? 0
239
+ : snapshots[0].duration;
240
+ const splice = await spliceVideoChain({
241
+ clips: snapshots,
242
+ outputPath,
243
+ workDir: sourceDir,
244
+ width: targetWidth,
245
+ height: targetHeight,
246
+ fps: firstFps,
247
+ overlap: resolvedOverlap,
248
+ });
249
+ const additional = clips.slice(2).map((clip) => clip.assetId);
250
+ const provenance = {
251
+ origin: "splice",
252
+ first_asset: clips[0].assetId,
253
+ second_asset: clips[1].assetId,
254
+ ...(additional.length > 0 ? { additional_assets: additional } : {}),
255
+ overlap_seconds: resolvedOverlap,
256
+ splice_timestamp: splice.timestamp,
257
+ match_frame_index: splice.frameIndex,
258
+ match_score: splice.score,
259
+ spliced_at: new Date().toISOString(),
260
+ };
261
+ await ingestWorkspaceOutputs(engine, assetId, [basename(outputPath)], "splice_video", provenance);
262
+ await engine.metadata.artifacts.write(assetId, "original", provenance);
263
+ return { provenance, timestamp: splice.timestamp, score: splice.score };
264
+ }
265
+ finally {
266
+ await rm(sourceDir, { recursive: true, force: true });
267
+ }
268
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
269
+ const processJobId = `process-video-${assetId}-${Date.now()}`;
270
+ await queue.enqueue(processJobId, {
271
+ type: "process_video",
272
+ videoPath: outputPath,
273
+ assetId,
274
+ assetDir,
275
+ bookId,
276
+ provenance: result.provenance,
277
+ });
278
+ return { success: true, splice_timestamp: result.timestamp, match_score: result.score };
279
+ });
280
+ }
@@ -0,0 +1,127 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { registerJobHandler, queue } from "../queue.js";
5
+ import { assertSaneTrimOutput } from "./trim-video.js";
6
+ import { AUDIO_OPTS, H264_MEDIUM, probeVideo, runFfmpeg } from "../ffmpeg.js";
7
+ async function buildChunk(engine, args) {
8
+ const { sourcePath, outputPath, start, end, assetId, provenance } = args;
9
+ const workspace = await mkdtemp(join(tmpdir(), "videobook-split-video-"));
10
+ const tempPath = join(workspace, "output.mp4");
11
+ try {
12
+ const startTime = start ?? 0;
13
+ const endTime = end ?? (await probeVideo(sourcePath)).videoDuration;
14
+ await runFfmpeg([
15
+ "-y", "-ss", String(startTime), "-i", sourcePath,
16
+ "-t", String(endTime - startTime),
17
+ "-map", "0:v:0", "-map", "0:a:0?",
18
+ ...H264_MEDIUM, ...AUDIO_OPTS, "-movflags", "+faststart", tempPath,
19
+ ]);
20
+ await assertSaneTrimOutput(sourcePath, tempPath);
21
+ const written = await engine.files.writeFromPath(assetId, outputPath.split("/").pop(), tempPath);
22
+ if (!written.ok)
23
+ throw new Error(written.error.message);
24
+ const recorded = await engine.history.recordOperation("split_video", assetId, provenance);
25
+ if (!recorded.ok)
26
+ throw new Error(recorded.error.message);
27
+ const metadata = await engine.metadata.artifacts.write(assetId, "original", provenance);
28
+ if (!metadata.ok)
29
+ throw new Error(metadata.error.message);
30
+ }
31
+ finally {
32
+ await rm(workspace, { recursive: true, force: true });
33
+ }
34
+ }
35
+ export function registerSplitVideoJob(engine) {
36
+ // The destination assets are brand-new, so no other job can hold a lease on
37
+ // them; we run the two chunks in parallel against the read-only source.
38
+ registerJobHandler("split_video", async (payload) => {
39
+ const { sourcePath, sourceAssetId, chunkAEnd, chunkBStart, assetIdA, assetDirA, outputPathA, assetIdB, assetDirB, outputPathB, bookId, } = payload;
40
+ const overlapSec = Math.max(0, chunkAEnd - chunkBStart);
41
+ const splitAt = (chunkAEnd + chunkBStart) / 2;
42
+ const splitAtIso = new Date().toISOString();
43
+ const provenanceA = {
44
+ origin: "split",
45
+ source_asset_id: sourceAssetId,
46
+ split_at: splitAt,
47
+ overlap_sec: overlapSec,
48
+ chunk: "a",
49
+ chunk_start: 0,
50
+ chunk_end: chunkAEnd,
51
+ split_at_iso: splitAtIso,
52
+ };
53
+ const provenanceB = {
54
+ origin: "split",
55
+ source_asset_id: sourceAssetId,
56
+ split_at: splitAt,
57
+ overlap_sec: overlapSec,
58
+ chunk: "b",
59
+ chunk_start: chunkBStart,
60
+ chunk_end: null,
61
+ split_at_iso: splitAtIso,
62
+ };
63
+ await Promise.all([
64
+ buildChunk(engine, {
65
+ sourcePath, outputPath: outputPathA,
66
+ start: undefined, end: chunkAEnd,
67
+ assetId: assetIdA,
68
+ provenance: provenanceA,
69
+ }),
70
+ buildChunk(engine, {
71
+ sourcePath, outputPath: outputPathB,
72
+ start: chunkBStart, end: undefined,
73
+ assetId: assetIdB,
74
+ provenance: provenanceB,
75
+ }),
76
+ ]);
77
+ // Both chunk media files already exist on disk at this point. Enqueue each
78
+ // process_video job independently (allSettled, not all): if one enqueue
79
+ // fails we must not abandon the other chunk's already-built media. Any
80
+ // chunk whose job fails to land is flagged as a recoverable error instead
81
+ // of being silently orphaned.
82
+ const chunks = [
83
+ {
84
+ label: "a",
85
+ jobId: `process-video-${assetIdA}-${Date.now()}`,
86
+ assetId: assetIdA,
87
+ assetDir: assetDirA,
88
+ videoPath: outputPathA,
89
+ provenance: provenanceA,
90
+ },
91
+ {
92
+ label: "b",
93
+ jobId: `process-video-${assetIdB}-${Date.now() + 1}`,
94
+ assetId: assetIdB,
95
+ assetDir: assetDirB,
96
+ videoPath: outputPathB,
97
+ provenance: provenanceB,
98
+ },
99
+ ];
100
+ const results = await Promise.allSettled(chunks.map((c) => queue.enqueue(c.jobId, {
101
+ type: "process_video",
102
+ videoPath: c.videoPath,
103
+ assetId: c.assetId,
104
+ assetDir: c.assetDir,
105
+ bookId,
106
+ provenance: c.provenance,
107
+ })));
108
+ const failed = [];
109
+ for (let i = 0; i < results.length; i++) {
110
+ const r = results[i];
111
+ if (r.status === "rejected") {
112
+ const c = chunks[i];
113
+ const reason = r.reason instanceof Error ? r.reason.message : String(r.reason);
114
+ await engine.jobs.failures.write(c.assetId, {
115
+ message: `split chunk ${c.label} was built but could not be queued for processing: ${reason}`,
116
+ failCode: "split_enqueue_failed",
117
+ });
118
+ failed.push(`chunk ${c.label} (${c.assetId})`);
119
+ }
120
+ }
121
+ if (failed.length > 0) {
122
+ throw new Error(`split_video: failed to enqueue ${failed.join(", ")} for processing; ` +
123
+ `affected chunk(s) flagged as recoverable`);
124
+ }
125
+ return { success: true, chunk_a: assetIdA, chunk_b: assetIdB };
126
+ });
127
+ }