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,100 @@
1
+ import { registerJobHandler } from "../queue.js";
2
+ async function statusOf(engine, assetId) {
3
+ const res = await engine.status.get(assetId);
4
+ return res.ok ? res.value : "error";
5
+ }
6
+ /**
7
+ * Restore one asset to its last usable state before the current error.
8
+ *
9
+ * Runs lock-free, exactly like restore_asset_version: recoverAssetRow and
10
+ * restoreAsset re-derive the asset row themselves, and a held lock would make
11
+ * recoverAssetRow treat the asset as "abandoned". Concurrency is guarded by a
12
+ * read-only check for a live work lease instead.
13
+ *
14
+ * Tier 1 (non-destructive): clear the durable failure signals — generation
15
+ * error, stale pending task, expired lock — then re-derive the row from disk.
16
+ * If the media on disk is intact, this *is* the last usable state and no file
17
+ * changes are needed.
18
+ *
19
+ * Tier 2 (Dolt restore): the current manifest itself is broken, so walk the
20
+ * asset history newest-first and restore the first revision that yields a `ready`
21
+ * asset. If none do, put the asset back on the revision the fix started from
22
+ * so a failed walk cannot delete the only copy of the media.
23
+ */
24
+ async function fixAsset(engine, assetId) {
25
+ // Refuse to touch an asset a job is actively working (live, unexpired lease).
26
+ const work = await engine.jobs.artifactWork.read(assetId);
27
+ if (work.ok &&
28
+ work.value &&
29
+ work.value.status === "working" &&
30
+ work.value.deadlineAt != null &&
31
+ work.value.deadlineAt > Date.now()) {
32
+ return { status: "busy", asset_id: assetId, reason: "asset has an active job lease" };
33
+ }
34
+ const before = await statusOf(engine, assetId);
35
+ // Tier 1 — clear durable failure signals, then re-derive from disk.
36
+ await engine.jobs.failures.clear(assetId);
37
+ const pending = await engine.jobs.pending.read(assetId);
38
+ if (pending.ok && pending.value) {
39
+ // delete (not forceFail) so we don't write a fresh generation error.
40
+ await engine.jobs.pending.delete(assetId);
41
+ }
42
+ const dirRes = await engine.workspaces.resolveArtifact(assetId);
43
+ if (dirRes.ok)
44
+ await engine.jobs.locks.cleanStale(dirRes.value);
45
+ await engine.jobs.recoverArtifact(assetId);
46
+ const afterTier1 = await statusOf(engine, assetId);
47
+ if (afterTier1 === "ready") {
48
+ return {
49
+ status: before === "ready" ? "already_healthy" : "recovered_in_place",
50
+ asset_id: assetId,
51
+ asset_status: afterTier1,
52
+ };
53
+ }
54
+ // Tier 2 — restore from immutable Dolt history using stable identity.
55
+ const artifact = engine.artifacts.get(assetId);
56
+ if (!artifact.ok) {
57
+ return {
58
+ status: "unfixable",
59
+ asset_id: assetId,
60
+ scanned: 0,
61
+ asset_status: afterTier1,
62
+ };
63
+ }
64
+ const baseline = engine.head;
65
+ const history = engine.history.artifact(artifact.value.artifactId);
66
+ for (const commit of history) {
67
+ if (commit.hash === baseline)
68
+ continue;
69
+ const restored = await engine.history.restoreArtifact(artifact.value.artifactId, commit.hash);
70
+ if (!restored.ok)
71
+ continue;
72
+ // restoreAsset re-derives the row; clear any error it carried forward.
73
+ await engine.jobs.failures.clear(assetId);
74
+ const status = await statusOf(engine, assetId);
75
+ if (status === "ready") {
76
+ return {
77
+ status: "restored",
78
+ asset_id: assetId,
79
+ from_commit: commit.hash,
80
+ commit_date: commit.date,
81
+ asset_status: status,
82
+ };
83
+ }
84
+ }
85
+ if (engine.head !== baseline) {
86
+ await engine.history.restoreArtifact(artifact.value.artifactId, baseline);
87
+ }
88
+ return {
89
+ status: "unfixable",
90
+ asset_id: assetId,
91
+ scanned: history.length,
92
+ asset_status: await statusOf(engine, assetId),
93
+ };
94
+ }
95
+ export function registerDoctorFixJob(engine) {
96
+ registerJobHandler("doctor_fix", async (payload) => {
97
+ const { assetId } = payload;
98
+ return fixAsset(engine, assetId);
99
+ });
100
+ }
@@ -0,0 +1,427 @@
1
+ import { join } from "node:path";
2
+ import { stat, unlink } from "node:fs/promises";
3
+ import { YtDlp } from "ytdlp-nodejs";
4
+ import { ytdlpRunOptions } from "../ytdlp-binary.js";
5
+ import { extractOpenGraph } from "@devmehq/open-graph-extractor";
6
+ import { queue, registerJobHandler } from "../queue.js";
7
+ import { runFfmpeg, probeVideo, H264_MEDIUM, AUDIO_OPTS } from "../ffmpeg.js";
8
+ import { downloadUrlIntoAsset } from "./_download-into-asset.js";
9
+ import { withLockedLocalLease } from "./_lease.js";
10
+ import { ingestWorkspaceOutputs, replaceArtifactFileFromPath, } from "./_persist-output.js";
11
+ import { assertSaneTrimOutput, trimRange, trimRangeExact, } from "./trim-video.js";
12
+ const LOCK_DURATION_MS = 600_000;
13
+ // Hidden filename so a failed video download doesn't leave behind an
14
+ // `original.jpg` that sync's `findUnprocessedUploads` would pick up and
15
+ // process as if the asset were a successful image upload (origin "external").
16
+ const OG_IMAGE_FILENAME = ".opengraph.jpg";
17
+ const OG_PLATFORM_SUFFIX = / on (?:X|Twitter|Instagram|Facebook|YouTube|TikTok|Vimeo|Reddit|Twitch|Dailymotion)$/i;
18
+ const OG_HANDLE_PAREN = / \(@?\w+\)/g;
19
+ function cleanOgTitle(raw) {
20
+ return raw.replace(OG_PLATFORM_SUFFIX, "").replace(OG_HANDLE_PAREN, "").trim();
21
+ }
22
+ export async function validateDownloadOutput(outputPath) {
23
+ const partPath = outputPath + ".part";
24
+ let fileStat = null;
25
+ try {
26
+ fileStat = await stat(outputPath);
27
+ }
28
+ catch {
29
+ // output file doesn't exist — check for leftover .part
30
+ }
31
+ if (fileStat) {
32
+ if (fileStat.size === 0) {
33
+ try {
34
+ await unlink(outputPath);
35
+ }
36
+ catch { /* best-effort cleanup */ }
37
+ throw new Error("Download produced an empty file");
38
+ }
39
+ // yt-dlp can finalize a non-empty mp4 that is structurally broken.
40
+ // Probe the container so process_video doesn't get a non-decodable file.
41
+ try {
42
+ await probeVideo(outputPath);
43
+ }
44
+ catch (error) {
45
+ try {
46
+ await unlink(outputPath);
47
+ }
48
+ catch { /* best-effort cleanup */ }
49
+ const msg = error instanceof Error ? error.message : String(error);
50
+ throw new Error(`Download produced an unreadable video: ${msg}`);
51
+ }
52
+ return;
53
+ }
54
+ // Output file missing — check for .part file
55
+ let hasPartFile = false;
56
+ try {
57
+ await stat(partPath);
58
+ hasPartFile = true;
59
+ }
60
+ catch {
61
+ // no .part file either
62
+ }
63
+ if (hasPartFile) {
64
+ try {
65
+ await unlink(partPath);
66
+ }
67
+ catch { /* best-effort cleanup */ }
68
+ throw new Error("Download incomplete: only a partial .part file was found");
69
+ }
70
+ throw new Error("Download failed: output file was not created");
71
+ }
72
+ async function blurFillExport(inputPath, outputPath, targetWidth, targetHeight) {
73
+ const filterComplex = [
74
+ `[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=increase,crop=${targetWidth}:${targetHeight},boxblur=20:5[bg]`,
75
+ `[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease[fg]`,
76
+ `[bg][fg]overlay=(W-w)/2:(H-h)/2`,
77
+ ].join(";");
78
+ await runFfmpeg([
79
+ "-y", "-i", inputPath,
80
+ "-filter_complex", filterComplex,
81
+ ...H264_MEDIUM, ...AUDIO_OPTS,
82
+ outputPath,
83
+ ]);
84
+ }
85
+ function extractOgImageUrl(ogImage) {
86
+ if (!ogImage)
87
+ return null;
88
+ if (typeof ogImage === "string")
89
+ return ogImage;
90
+ if (Array.isArray(ogImage)) {
91
+ const first = ogImage[0];
92
+ if (!first)
93
+ return null;
94
+ if (typeof first === "string")
95
+ return first;
96
+ return typeof first === "object" && "url" in first ? first.url : null;
97
+ }
98
+ return typeof ogImage === "object" && "url" in ogImage ? ogImage.url : null;
99
+ }
100
+ async function readOpenGraphData(engine, assetId) {
101
+ const result = await engine.files.read(assetId, ".opengraph.json");
102
+ if (!result.ok)
103
+ return null;
104
+ try {
105
+ return JSON.parse(result.value.toString());
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ async function loadOpenGraphData(engine, url, assetId) {
112
+ try {
113
+ const response = await fetch(url, {
114
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; VideobookBot/1.0)" },
115
+ signal: AbortSignal.timeout(10_000),
116
+ redirect: "follow",
117
+ });
118
+ const ogData = extractOpenGraph(await response.text());
119
+ await engine.files.write(assetId, ".opengraph.json", Buffer.from(JSON.stringify(ogData, null, 2)));
120
+ return ogData;
121
+ }
122
+ catch {
123
+ return readOpenGraphData(engine, assetId);
124
+ }
125
+ }
126
+ async function stageOpenGraphImage(engine, ogData, assetId) {
127
+ const imageUrl = extractOgImageUrl(ogData.ogImage);
128
+ if (!imageUrl)
129
+ return;
130
+ try {
131
+ await downloadUrlIntoAsset(engine, imageUrl, assetId, OG_IMAGE_FILENAME, {
132
+ kind: "OG image",
133
+ fetchInit: { signal: AbortSignal.timeout(15_000), redirect: "follow" },
134
+ });
135
+ }
136
+ catch (error) {
137
+ void error;
138
+ }
139
+ }
140
+ const IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "webp", "gif"]);
141
+ export async function applyRequestedDownloadRange(engine, assetId, assetDir, outputPath, requestedRange) {
142
+ const start = requestedRange.start ?? 0;
143
+ const end = requestedRange.end;
144
+ if (!Number.isFinite(start) || start < 0
145
+ || (end !== undefined && (!Number.isFinite(end) || end <= start))) {
146
+ throw new Error("Requested download range is invalid");
147
+ }
148
+ if (start === 0 && end === undefined)
149
+ return false;
150
+ const rangeFilename = ".requested-range.mp4";
151
+ const rangePath = join(assetDir, rangeFilename);
152
+ try {
153
+ if (end !== undefined) {
154
+ await trimRangeExact(outputPath, rangePath, start, end - start);
155
+ }
156
+ else {
157
+ await trimRange(outputPath, rangePath, start, undefined);
158
+ }
159
+ await assertSaneTrimOutput(outputPath, rangePath);
160
+ await replaceArtifactFileFromPath(engine, assetId, "original.mp4", rangePath);
161
+ return true;
162
+ }
163
+ catch (error) {
164
+ await engine.files.delete(assetId, rangeFilename);
165
+ throw error;
166
+ }
167
+ }
168
+ export function isImageContent(info) {
169
+ if (!info.ext)
170
+ return false;
171
+ if (!IMAGE_EXTENSIONS.has(info.ext.toLowerCase()))
172
+ return false;
173
+ if (info.vcodec && info.vcodec !== "none")
174
+ return false;
175
+ return true;
176
+ }
177
+ export function imageExtension(info) {
178
+ const ext = info.ext?.toLowerCase() ?? "jpg";
179
+ return IMAGE_EXTENSIONS.has(ext) ? `.${ext}` : ".jpg";
180
+ }
181
+ export function registerDownloadVideoJob(engine) {
182
+ registerJobHandler("download_video", async (payload, ctx) => {
183
+ const ytdlp = new YtDlp(ytdlpRunOptions());
184
+ const { url, assetId, assetDir, exportVideos, sourcePrefix, requestedRange, } = payload;
185
+ const bookId = payload.bookId ?? "";
186
+ const outputPath = join(assetDir, "original.mp4");
187
+ return withLockedLocalLease(engine, assetId, assetDir, ctx, "download", { durationMs: LOCK_DURATION_MS, data: { state: "downloading" } }, async () => {
188
+ // (a) Pre-stage OG image as a hidden file. The b2 fallback (yt-dlp
189
+ // returns "no video") promotes this to original.jpg on a fresh image
190
+ // asset; a successful video download promotes it after step (d).
191
+ const ogData = await loadOpenGraphData(engine, url, assetId);
192
+ if (ogData) {
193
+ await stageOpenGraphImage(engine, ogData, assetId);
194
+ }
195
+ // (b) Fetch ytdlp metadata
196
+ let info;
197
+ try {
198
+ info = await ytdlp.getInfoAsync(url);
199
+ }
200
+ catch (error) {
201
+ const msg = error instanceof Error ? error.message : String(error);
202
+ // (b2) yt-dlp found no video — fall back to OG image as an image asset
203
+ if (/no video/i.test(msg)) {
204
+ const ogImageResult = await engine.files.read(assetId, OG_IMAGE_FILENAME);
205
+ if (!ogImageResult.ok)
206
+ throw new Error(`No video found and no OG image available: ${msg}`);
207
+ // Read OG metadata for asset naming and provenance
208
+ let ogTitle = "image";
209
+ const fallbackOgData = ogData ?? await readOpenGraphData(engine, assetId);
210
+ if (fallbackOgData?.ogTitle)
211
+ ogTitle = cleanOgTitle(fallbackOgData.ogTitle);
212
+ const assetName = [sourcePrefix, ogTitle].join(" ");
213
+ const imgResult = await engine.artifacts.create("image", assetName);
214
+ if (!imgResult.ok)
215
+ throw new Error(`Failed to create image asset: ${imgResult.error.message}`);
216
+ const imgAssetId = imgResult.value.artifactId;
217
+ // Copy OG image and metadata to new image asset
218
+ await engine.files.write(imgAssetId, "original.jpg", ogImageResult.value);
219
+ try {
220
+ await engine.files.copy(assetId, ".opengraph.json", imgAssetId, ".opengraph.json");
221
+ }
222
+ catch { /* non-fatal */ }
223
+ // Release lock and delete the old video asset
224
+ await engine.jobs.locks.release(assetDir);
225
+ await engine.artifacts.delete(assetId);
226
+ const provenance = {
227
+ origin: "download",
228
+ source_url: url,
229
+ title: fallbackOgData?.ogTitle ?? "",
230
+ uploader: "",
231
+ description: fallbackOgData?.ogDescription ?? "",
232
+ webpage_url: url,
233
+ platform: sourcePrefix,
234
+ thumbnail: "",
235
+ downloaded_at: new Date().toISOString(),
236
+ };
237
+ const processJobId = `process-image-${imgAssetId}-${Date.now()}`;
238
+ await queue.enqueue(processJobId, {
239
+ type: "process_image",
240
+ assetId: imgAssetId,
241
+ bookId,
242
+ extension: ".jpg",
243
+ provenance,
244
+ });
245
+ return { success: true, retyped: true, assetId: imgAssetId };
246
+ }
247
+ throw new Error(`Failed to fetch video info: ${msg}`);
248
+ }
249
+ // (c) Handle playlists — if multiple entries, create additional assets + jobs for entries 2+
250
+ let playlistItem;
251
+ if (info._type === "playlist" && Array.isArray(info.entries) && info.entries.length > 1) {
252
+ // Current job handles entry 1
253
+ playlistItem = 1;
254
+ // Enqueue separate jobs for entries 2+
255
+ for (let i = 1; i < info.entries.length; i++) {
256
+ const entry = info.entries[i];
257
+ const entryKind = isImageContent(entry) ? "image" : "video";
258
+ const entryName = `${sourcePrefix} ${entry.title ?? `${entryKind} ${i + 1}`}`;
259
+ const assetResult = await engine.artifacts.create(entryKind, entryName);
260
+ if (!assetResult.ok)
261
+ continue;
262
+ const { artifactId: newAssetId, path: newAssetDir } = assetResult.value;
263
+ const newFilename = "original.mp4";
264
+ const newOutputPath = join(newAssetDir, newFilename);
265
+ const newJobId = `download-${newAssetId}-${Date.now()}`;
266
+ await queue.enqueue(newJobId, {
267
+ type: "download_video",
268
+ url,
269
+ outputPath: newOutputPath,
270
+ assetId: newAssetId,
271
+ assetDir: newAssetDir,
272
+ bookId,
273
+ exportVideos,
274
+ full: true,
275
+ sourcePrefix,
276
+ ...(requestedRange ? { requestedRange } : {}),
277
+ _playlistItem: i + 1,
278
+ });
279
+ }
280
+ }
281
+ else if (info._type === "playlist" && Array.isArray(info.entries) && info.entries.length === 1) {
282
+ playlistItem = 1;
283
+ }
284
+ // Check for internal _playlistItem (set when this job was spawned for a playlist entry)
285
+ const internalPlaylistItem = payload._playlistItem;
286
+ if (internalPlaylistItem !== undefined) {
287
+ playlistItem = internalPlaylistItem;
288
+ }
289
+ // (d0) Detect image-only content — retype asset and route to process_image
290
+ const itemInfo = (playlistItem !== undefined && info._type === "playlist" && info.entries?.length)
291
+ ? (info.entries[playlistItem - 1] ?? info)
292
+ : info;
293
+ if (isImageContent(itemInfo)) {
294
+ let effectiveAssetId = assetId;
295
+ // Retype video asset to image if needed
296
+ const sourceArtifact = engine.artifacts.get(assetId);
297
+ if (sourceArtifact.ok && sourceArtifact.value.kind === "video") {
298
+ const assetName = [sourcePrefix, itemInfo.title ?? "image"].join(" ");
299
+ const imgResult = await engine.artifacts.create("image", assetName);
300
+ if (!imgResult.ok)
301
+ throw new Error(`Failed to create image asset: ${imgResult.error.message}`);
302
+ effectiveAssetId = imgResult.value.artifactId;
303
+ // Copy .opengraph.json to new asset
304
+ try {
305
+ await engine.files.copy(assetId, ".opengraph.json", effectiveAssetId, ".opengraph.json");
306
+ }
307
+ catch {
308
+ // non-fatal — OG data may not exist
309
+ }
310
+ // Release lock and delete the old video asset
311
+ await engine.jobs.locks.release(assetDir);
312
+ const delResult = await engine.artifacts.delete(assetId);
313
+ if (!delResult.ok) {
314
+ // non-fatal — an orphaned video asset is acceptable
315
+ }
316
+ }
317
+ // Download image directly from yt-dlp's resolved URL
318
+ const imageUrl = itemInfo.url ?? itemInfo.thumbnail ?? "";
319
+ if (!imageUrl)
320
+ throw new Error("No image URL found in content info");
321
+ const ext = imageExtension(itemInfo);
322
+ await downloadUrlIntoAsset(engine, imageUrl, effectiveAssetId, `original${ext}`, {
323
+ kind: "image",
324
+ fetchInit: { signal: AbortSignal.timeout(30_000), redirect: "follow" },
325
+ });
326
+ // Build provenance
327
+ const provenance = {
328
+ origin: "download",
329
+ source_url: url,
330
+ title: itemInfo.title ?? info.title ?? "",
331
+ uploader: itemInfo.uploader ?? info.uploader ?? "",
332
+ description: itemInfo.description ?? info.description ?? "",
333
+ webpage_url: itemInfo.webpage_url ?? info.webpage_url ?? "",
334
+ platform: itemInfo.extractor ?? info.extractor ?? "",
335
+ thumbnail: itemInfo.thumbnail ?? info.thumbnail ?? "",
336
+ downloaded_at: new Date().toISOString(),
337
+ };
338
+ // Enqueue process_image instead of process_video
339
+ const processJobId = `process-image-${effectiveAssetId}-${Date.now()}`;
340
+ await queue.enqueue(processJobId, {
341
+ type: "process_image",
342
+ assetId: effectiveAssetId,
343
+ bookId,
344
+ extension: ext,
345
+ provenance,
346
+ });
347
+ return { success: true, retyped: assetId !== effectiveAssetId, assetId: effectiveAssetId };
348
+ }
349
+ // (d) Regular ytdlp download
350
+ const builder = ytdlp
351
+ .download(url)
352
+ .setOutputTemplate(outputPath)
353
+ .addArgs("--merge-output-format", "mp4");
354
+ if (playlistItem !== undefined) {
355
+ builder.addArgs("--playlist-items", String(playlistItem));
356
+ }
357
+ else {
358
+ builder.addArgs("--no-playlist");
359
+ }
360
+ await builder.run();
361
+ // (d2) Validate download output
362
+ await validateDownloadOutput(outputPath);
363
+ const rangeApplied = requestedRange
364
+ ? await applyRequestedDownloadRange(engine, assetId, assetDir, outputPath, requestedRange)
365
+ : false;
366
+ if (rangeApplied)
367
+ await validateDownloadOutput(outputPath);
368
+ // (d3) Promote staged OG image to original.jpg so process_video uses
369
+ // the curated platform thumbnail (e.g. YouTube maxres) rather than a
370
+ // first-frame extraction. Best-effort — process_video falls back to a
371
+ // first-frame thumbnail when original.jpg is absent.
372
+ try {
373
+ const ogImageResult = await engine.files.read(assetId, OG_IMAGE_FILENAME);
374
+ if (ogImageResult.ok) {
375
+ await engine.files.write(assetId, "original.jpg", ogImageResult.value);
376
+ }
377
+ }
378
+ catch { /* non-fatal */ }
379
+ // (e) Optional exports
380
+ const exports = [];
381
+ if (exportVideos) {
382
+ try {
383
+ const landscape = join(assetDir, "original_16x9.mp4");
384
+ await blurFillExport(outputPath, landscape, 1920, 1080);
385
+ exports.push("original_16x9.mp4");
386
+ }
387
+ catch {
388
+ // export failure is non-fatal
389
+ }
390
+ try {
391
+ const portrait = join(assetDir, "original_9x16.mp4");
392
+ await blurFillExport(outputPath, portrait, 1080, 1920);
393
+ exports.push("original_9x16.mp4");
394
+ }
395
+ catch {
396
+ // export failure is non-fatal
397
+ }
398
+ }
399
+ // (f) Build provenance from ytdlp metadata
400
+ const provenance = {
401
+ origin: "download",
402
+ source_url: url,
403
+ title: info.title ?? "",
404
+ uploader: info.uploader ?? "",
405
+ description: info.description ?? "",
406
+ webpage_url: info.webpage_url ?? "",
407
+ platform: info.extractor ?? "",
408
+ thumbnail: info.thumbnail ?? "",
409
+ full: !rangeApplied,
410
+ ...(requestedRange ? { requested_range: requestedRange } : {}),
411
+ downloaded_at: new Date().toISOString(),
412
+ };
413
+ await ingestWorkspaceOutputs(engine, assetId, ["original.mp4", ...exports], "download_video", { sourceUrl: url, exports });
414
+ // (g) Enqueue process_video
415
+ const processJobId = `process-video-${assetId}-${Date.now()}`;
416
+ await queue.enqueue(processJobId, {
417
+ type: "process_video",
418
+ videoPath: outputPath,
419
+ assetId,
420
+ assetDir,
421
+ bookId,
422
+ provenance,
423
+ });
424
+ return { success: true, exports };
425
+ }, { durationMs: LOCK_DURATION_MS });
426
+ });
427
+ }
@@ -0,0 +1,24 @@
1
+ import { findPrimaryMediaFile } from "videobook-engine";
2
+ import { appendDiagnostic } from "../diagnostics.js";
3
+ import { enqueueOptions } from "../queue.js";
4
+ export async function enqueueSearchIndex(engine, artifactId) {
5
+ try {
6
+ const artifact = engine.artifacts.get(artifactId);
7
+ if (!artifact.ok)
8
+ throw new Error(artifact.error.message);
9
+ if (!["image", "video", "audio", "final"].includes(artifact.value.kind))
10
+ return;
11
+ const manifest = await engine.withLocalMedia(() => engine.files.manifest(artifactId));
12
+ if (!manifest.ok)
13
+ throw Object.assign(new Error(manifest.error.message), { name: manifest.error.code });
14
+ const source = findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
15
+ if (!source)
16
+ throw new Error("No media source is available for indexing");
17
+ engine.jobs.queue.enqueue(enqueueOptions(`semantic-index:${artifactId}:${source.objectHash}`, {
18
+ type: "semantic_index", prepareVisualMedia: true, assetId: artifactId, bookId: engine.book.get().bookId,
19
+ }));
20
+ }
21
+ catch (error) {
22
+ await appendDiagnostic(engine, "semantic-index-deferred", { artifactId, error }).catch(() => undefined);
23
+ }
24
+ }
@@ -0,0 +1,122 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join, relative } from "node:path";
4
+ import { registerJobHandler, queue } from "../queue.js";
5
+ import { trimRange, trimRangeExact, assertSaneTrimOutput } from "./trim-video.js";
6
+ import { ingestWorkspaceOutputs } from "./_persist-output.js";
7
+ import { validateMediaOutputPaths, withMediaOutputLock } from "../media-output-path.js";
8
+ async function buildChunk(engine, args) {
9
+ const { sourcePath, outputPath, start, end, assetId, provenance, exact, skipMetadata } = args;
10
+ try {
11
+ if (exact) {
12
+ await trimRangeExact(sourcePath, outputPath, start, end - start);
13
+ }
14
+ else {
15
+ await trimRange(sourcePath, outputPath, start, end);
16
+ }
17
+ await assertSaneTrimOutput(sourcePath, outputPath);
18
+ }
19
+ catch (err) {
20
+ await rm(outputPath, { force: true });
21
+ throw err;
22
+ }
23
+ if (!skipMetadata) {
24
+ await ingestWorkspaceOutputs(engine, assetId, [outputPath.split("/").pop()], "even_split_chunk", provenance);
25
+ await engine.metadata.artifacts.write(assetId, "original", provenance);
26
+ }
27
+ }
28
+ async function splitIntoDestination(engine, payload, ctx) {
29
+ const assetId = payload.destination.assetId;
30
+ const manifest = await engine.files.manifest(assetId);
31
+ if (!manifest.ok)
32
+ throw new Error(manifest.error.message);
33
+ const assetDir = manifest.value.path;
34
+ return withMediaOutputLock(engine, assetDir, ctx.signal, async () => {
35
+ const filenames = await validateMediaOutputPaths(engine, assetId, payload.chunks.map((chunk) => relative(assetDir, chunk.outputPath)), ".mp4");
36
+ const workspace = await mkdtemp(join(tmpdir(), "videobook-split-destination-"));
37
+ try {
38
+ const outputs = payload.chunks.map((_, index) => join(workspace, `${index}.mp4`));
39
+ const built = await Promise.allSettled(payload.chunks.map((chunk, index) => buildChunk(engine, {
40
+ sourcePath: payload.sourcePath,
41
+ outputPath: outputs[index],
42
+ start: chunk.start,
43
+ end: chunk.end,
44
+ assetId,
45
+ provenance: {},
46
+ exact: chunk.exact,
47
+ skipMetadata: true,
48
+ })));
49
+ const failed = built.find((result) => result.status === "rejected");
50
+ if (failed?.status === "rejected")
51
+ throw failed.reason;
52
+ ctx.signal?.throwIfAborted();
53
+ for (const [index, filename] of filenames.entries()) {
54
+ const written = await engine.files.writeFromPath(assetId, filename, outputs[index]);
55
+ if (!written.ok)
56
+ throw new Error(written.error.message);
57
+ }
58
+ const recorded = await engine.history.recordOperation("even_split", assetId, {
59
+ source_asset_id: payload.sourceAssetId,
60
+ filenames,
61
+ chunk_count: payload.chunks.length,
62
+ split_at_iso: new Date().toISOString(),
63
+ });
64
+ if (!recorded.ok)
65
+ throw new Error(recorded.error.message);
66
+ return { success: true, destination_asset_id: assetId, filenames };
67
+ }
68
+ finally {
69
+ await rm(workspace, { recursive: true, force: true });
70
+ }
71
+ });
72
+ }
73
+ export function registerEvenSplitJob(engine) {
74
+ registerJobHandler("even_split", async (payload, ctx) => {
75
+ const { sourcePath, sourceAssetId, chunks, bookId, destination } = payload;
76
+ if (destination)
77
+ return splitIntoDestination(engine, payload, ctx);
78
+ const splitAtIso = new Date().toISOString();
79
+ const buildPromises = chunks.map((chunk, i) => {
80
+ const provenance = {
81
+ origin: "even_split",
82
+ source_asset_id: sourceAssetId,
83
+ chunk_index: i,
84
+ chunk_count: chunks.length,
85
+ chunk_start: chunk.start,
86
+ chunk_end: chunk.end,
87
+ chunk_duration: chunk.duration,
88
+ split_at_iso: splitAtIso,
89
+ };
90
+ // Targeted chunks need exact-second container duration; the trailing
91
+ // leftover does not (and the user explicitly doesn't care).
92
+ return buildChunk(engine, {
93
+ sourcePath,
94
+ outputPath: chunk.outputPath,
95
+ start: chunk.start,
96
+ end: chunk.end,
97
+ assetId: chunk.assetId,
98
+ provenance,
99
+ exact: chunk.exact,
100
+ });
101
+ });
102
+ await Promise.all(buildPromises);
103
+ await Promise.all(chunks.map((chunk, i) => queue.enqueueAndWait(`process-video-${chunk.assetId}-${Date.now() + i}`, {
104
+ type: "process_video",
105
+ videoPath: chunk.outputPath,
106
+ assetId: chunk.assetId,
107
+ assetDir: chunk.assetDir,
108
+ bookId,
109
+ skipDownstreamJobs: true,
110
+ provenance: {
111
+ origin: "even_split",
112
+ source_asset_id: sourceAssetId,
113
+ chunk_index: i,
114
+ chunk_count: chunks.length,
115
+ },
116
+ })));
117
+ return {
118
+ success: true,
119
+ chunk_asset_ids: chunks.map((c) => c.assetId),
120
+ };
121
+ });
122
+ }