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,185 @@
1
+ import { join } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+ import { writeFile, unlink, readFile as readFsFile } from "node:fs/promises";
4
+ import { logger } from "../logger.js";
5
+ import { registerJobHandler } from "../queue.js";
6
+ import { runFfmpeg } from "../ffmpeg.js";
7
+ import { completeMediaGeneration, providerResultUrls } from "../media/complete.js";
8
+ import { MEDIA_TASK_TYPES } from "../media/adapters/types.js";
9
+ import { legacyMediaForTaskType, legacyMediaMeta } from "../media/legacy-task-types.js";
10
+ const LOCK_DURATION_MS = 300_000;
11
+ const TASK_TYPES_BY_COMPLETION_FAMILY = {
12
+ isolateVocals: ["isolate_vocals"],
13
+ media: Object.values(MEDIA_TASK_TYPES),
14
+ };
15
+ const COMPLETION_FAMILY_BY_TASK_TYPE = new Map(Object.entries(TASK_TYPES_BY_COMPLETION_FAMILY).flatMap(([family, taskTypes]) => taskTypes.map((taskType) => [taskType, family])));
16
+ export function completionFamilyForTaskType(taskType) {
17
+ return COMPLETION_FAMILY_BY_TASK_TYPE.get(taskType)
18
+ ?? (legacyMediaForTaskType(taskType) ? "media" : undefined);
19
+ }
20
+ async function reclaimFailedCompletion(engine, task, ctx) {
21
+ const { assetId, taskId, taskType, meta } = task;
22
+ const failure = engine.jobs.failures.read(assetId);
23
+ if (!failure.ok || !failure.value)
24
+ return null;
25
+ const pending = engine.jobs.pending.read(assetId);
26
+ if (!pending.ok || pending.value)
27
+ return null;
28
+ const cleared = await engine.jobs.failures.clear(assetId);
29
+ if (!cleared.ok || !cleared.value)
30
+ return null;
31
+ const lease = engine.jobs.artifactWork.begin(assetId, {
32
+ kind: "generate",
33
+ ownerKind: "job",
34
+ durationMs: LOCK_DURATION_MS,
35
+ });
36
+ if (!lease)
37
+ throw new Error(`Could not re-claim ${assetId} to retry its completion`);
38
+ ctx.setLease({ kind: "owner", ownerId: lease.ownerId });
39
+ const written = await engine.jobs.pending.write({ artifactId: assetId, taskId, taskType, meta }, lease.ownerId);
40
+ if (!written.ok) {
41
+ throw new Error(`Failed to re-register provider task ${taskId} for ${assetId}: ${written.error.message}`);
42
+ }
43
+ if (written.value === null) {
44
+ throw new Error(`Lost the lease on ${assetId} while re-registering provider task ${taskId}`);
45
+ }
46
+ return lease.ownerId;
47
+ }
48
+ export function registerCompleteProviderTaskJob(engine) {
49
+ registerJobHandler("complete_kie_task", async (payload, ctx) => {
50
+ const { taskId, taskType, assetId, bookId, meta, resultUrls, resultObject, } = payload;
51
+ // Pattern C: inherit the provider lease via getOwner, abort if the row
52
+ // already terminated, and register the lease so the wrapper's catch path
53
+ // CAS-fails by task_id (unaffected by a fresh retry on the same asset).
54
+ // A row that was failed by an earlier completion attempt is re-claimed
55
+ // instead, so retrying the job after the cause is fixed finishes the
56
+ // asset rather than silently skipping it.
57
+ const ownerId = engine.jobs.pending.getOwner(assetId, taskId)
58
+ ?? await reclaimFailedCompletion(engine, { assetId, taskId, taskType, meta }, ctx);
59
+ if (!ownerId) {
60
+ // The row was already removed by a delete or a stale duplicate
61
+ // dispatch. Either way the task is no longer ours to complete.
62
+ return { success: true, skipped: true };
63
+ }
64
+ ctx.setLease({ kind: "task", taskId });
65
+ const completionFamily = completionFamilyForTaskType(taskType);
66
+ if (!completionFamily) {
67
+ throw new Error(`Unhandled provider task type: ${taskType}`);
68
+ }
69
+ // Resolve the asset directory at runtime so renames don't break completion.
70
+ const assetDirResult = await engine.workspaces.resolveArtifact(assetId);
71
+ if (!assetDirResult.ok) {
72
+ await engine.jobs.pending.delete(assetId, taskId);
73
+ return { success: true, skipped: true };
74
+ }
75
+ const assetDir = assetDirResult.value;
76
+ const lockState = taskType === "isolate_vocals" ? "isolating" : "generating";
77
+ const lockResult = await engine.jobs.locks.acquire(assetDir, { durationMs: LOCK_DURATION_MS, data: { state: lockState } });
78
+ if (!lockResult.ok) {
79
+ // LOCKED means another completion job for this same asset is already
80
+ // running (sync can enqueue duplicates between polls because
81
+ // the provider task remains pending until the completion job finishes).
82
+ // Treat as a no-op — the other job will finish the work.
83
+ if (lockResult.error.code === "LOCKED") {
84
+ return { success: true, skipped: true };
85
+ }
86
+ throw new Error(`Asset is locked: ${lockResult.error.message}`);
87
+ }
88
+ // Renew the provider lease while we work — covers downloads + processing.
89
+ const heartbeat = setInterval(() => {
90
+ try {
91
+ engine.jobs.artifactWork.renew(assetId, ownerId, 60_000);
92
+ }
93
+ catch (error) {
94
+ logger.warn("heartbeat-failed", { assetId, error });
95
+ }
96
+ }, 20_000);
97
+ const urls = completionFamily === "media"
98
+ ? providerResultUrls(resultUrls, resultObject)
99
+ : resultUrls;
100
+ try {
101
+ switch (completionFamily) {
102
+ case "isolateVocals":
103
+ await completeIsolateVocals(engine, assetId, meta, urls);
104
+ break;
105
+ case "media":
106
+ await completeMediaGeneration({
107
+ engine,
108
+ assetId,
109
+ assetDir,
110
+ bookId,
111
+ meta: legacyMediaMeta(taskType, meta),
112
+ outputs: urls.map((url) => ({ url })),
113
+ });
114
+ break;
115
+ default: {
116
+ const unhandledFamily = completionFamily;
117
+ throw new Error(`Unhandled provider completion family: ${unhandledFamily}`);
118
+ }
119
+ }
120
+ await engine.jobs.pending.delete(assetId, taskId);
121
+ return { success: true };
122
+ }
123
+ finally {
124
+ clearInterval(heartbeat);
125
+ await engine.jobs.locks.release(assetDir);
126
+ }
127
+ });
128
+ }
129
+ async function completeIsolateVocals(engine, assetId, meta, resultUrls) {
130
+ const vocalsUrl = resultUrls[0];
131
+ if (!vocalsUrl)
132
+ throw new Error("No vocals URL in result");
133
+ const response = await fetch(vocalsUrl);
134
+ if (!response.ok)
135
+ throw new Error(`Failed to download vocals: ${response.status} ${response.statusText}`);
136
+ let vocalsBuffer = Buffer.from(await response.arrayBuffer());
137
+ // Trim back to original duration if audio was padded before upload
138
+ const originalDuration = meta.originalDuration;
139
+ if (originalDuration !== undefined) {
140
+ const tmpIn = join(tmpdir(), `isolate-trim-in-${Date.now()}.mp3`);
141
+ const tmpOut = join(tmpdir(), `isolate-trim-out-${Date.now()}.mp3`);
142
+ try {
143
+ await writeFile(tmpIn, vocalsBuffer);
144
+ await runFfmpeg(["-y", "-i", tmpIn, "-t", String(originalDuration), tmpOut]);
145
+ vocalsBuffer = await readFsFile(tmpOut);
146
+ }
147
+ finally {
148
+ await unlink(tmpIn).catch(() => { });
149
+ await unlink(tmpOut).catch(() => { });
150
+ }
151
+ }
152
+ const isAudioAsset = meta.isAudioAsset;
153
+ if (isAudioAsset) {
154
+ // Audio assets: write mix.mp3 and .mix.json
155
+ await engine.files.write(assetId, "mix.mp3", vocalsBuffer);
156
+ const mixSettings = { volume: 100, fadeIn: 0, fadeOut: 0 };
157
+ await engine.files.write(assetId, ".mix.json", JSON.stringify(mixSettings, null, 2));
158
+ }
159
+ else {
160
+ // Video assets: write audio_vocals.mp3
161
+ await engine.files.write(assetId, "audio_vocals.mp3", vocalsBuffer);
162
+ // Optionally write background audio if a second URL exists
163
+ if (resultUrls[1]) {
164
+ const bgResponse = await fetch(resultUrls[1]);
165
+ if (bgResponse.ok) {
166
+ let bgBuffer = Buffer.from(await bgResponse.arrayBuffer());
167
+ if (originalDuration !== undefined) {
168
+ const tmpBgIn = join(tmpdir(), `isolate-bg-in-${Date.now()}.mp3`);
169
+ const tmpBgOut = join(tmpdir(), `isolate-bg-out-${Date.now()}.mp3`);
170
+ try {
171
+ await writeFile(tmpBgIn, bgBuffer);
172
+ await runFfmpeg(["-y", "-i", tmpBgIn, "-t", String(originalDuration), tmpBgOut]);
173
+ bgBuffer = await readFsFile(tmpBgOut);
174
+ }
175
+ finally {
176
+ await unlink(tmpBgIn).catch(() => { });
177
+ await unlink(tmpBgOut).catch(() => { });
178
+ }
179
+ }
180
+ await engine.files.write(assetId, "audio_background.mp3", bgBuffer);
181
+ }
182
+ }
183
+ }
184
+ await engine.history.recordOperation("isolate_vocals", assetId, { mode: "cloud" });
185
+ }
@@ -0,0 +1,33 @@
1
+ import { copyAndAttachLibraryAsset, copyLibraryAsset, } from "../media-library.js";
2
+ import { registerJobHandler } from "../queue.js";
3
+ function requiredString(payload, key) {
4
+ const value = payload[key];
5
+ if (typeof value !== "string" || value.length === 0) {
6
+ throw new Error(`copy_library_asset requires ${key}`);
7
+ }
8
+ return value;
9
+ }
10
+ export function registerCopyLibraryAssetJob(books) {
11
+ registerJobHandler("copy_library_asset", async (payload) => {
12
+ const input = {
13
+ sourceBookId: requiredString(payload, "sourceBookId"),
14
+ sourceArtifactId: requiredString(payload, "sourceArtifactId"),
15
+ targetBookId: requiredString(payload, "targetBookId"),
16
+ ...(typeof payload.newName === "string"
17
+ ? { newName: payload.newName }
18
+ : {}),
19
+ };
20
+ if (typeof payload.notebookId !== "string"
21
+ || typeof payload.cellId !== "string") {
22
+ return await copyLibraryAsset(books, input);
23
+ }
24
+ return await copyAndAttachLibraryAsset(books, {
25
+ ...input,
26
+ notebookId: payload.notebookId,
27
+ cellId: payload.cellId,
28
+ expectedOutputArtifactId: typeof payload.expectedOutputArtifactId === "string"
29
+ ? payload.expectedOutputArtifactId
30
+ : null,
31
+ });
32
+ });
33
+ }
@@ -0,0 +1,113 @@
1
+ import { computeEstimate } from "@apicity/cost";
2
+ import { logger } from "../logger.js";
3
+ import { appendDiagnostic, diagnosticRecord } from "../diagnostics.js";
4
+ import { estimateCostDef } from "../tools/estimate-cost.js";
5
+ function toUpstreamCostHints(hints) {
6
+ return {
7
+ ...(typeof hints.duration === "number" && hints.duration > 0 ? { durationSeconds: hints.duration } : {}),
8
+ ...(typeof hints.inputDuration === "number" && hints.inputDuration > 0 ? { inputDurationSeconds: hints.inputDuration } : {}),
9
+ };
10
+ }
11
+ function assessComputedEstimate(estimate) {
12
+ const warnings = estimate.warnings.filter((warning) => typeof warning === "string" && warning.length > 0);
13
+ if (estimate.usd > 0) {
14
+ return { available: true, usd: estimate.usd, warnings };
15
+ }
16
+ if (warnings.length === 0 && estimate.usd === 0) {
17
+ return { available: true, usd: 0, warnings };
18
+ }
19
+ return {
20
+ available: false,
21
+ usd: estimate.usd,
22
+ warnings,
23
+ reason: classifyUnavailable(warnings, estimate.usd),
24
+ };
25
+ }
26
+ export function assessBestEffortEstimate(compute) {
27
+ try {
28
+ return assessComputedEstimate(compute());
29
+ }
30
+ catch {
31
+ return {
32
+ available: false,
33
+ warnings: [],
34
+ reason: "estimator-threw",
35
+ };
36
+ }
37
+ }
38
+ export function assessKieRequestEstimate(request, costHints = {}) {
39
+ return assessBestEffortEstimate(() => computeEstimate({
40
+ provider: "kie",
41
+ payload: request,
42
+ costHints: toUpstreamCostHints(costHints),
43
+ }));
44
+ }
45
+ export async function assessToolCallEstimate(engine, tool, input) {
46
+ try {
47
+ const result = await estimateCostDef(engine).handler({
48
+ calls: [{ tool_name: tool, input }],
49
+ });
50
+ if (result.isError) {
51
+ return {
52
+ available: false,
53
+ warnings: [],
54
+ reason: "estimate-error",
55
+ };
56
+ }
57
+ const text = result.content.find((item) => item.type === "text")?.text;
58
+ const parsed = text
59
+ ? JSON.parse(text)
60
+ : {};
61
+ const warnings = Array.isArray(parsed.estimate?.warnings)
62
+ ? parsed.estimate.warnings.filter((warning) => typeof warning === "string")
63
+ : [];
64
+ const usd = typeof parsed.estimate?.usd === "number"
65
+ ? parsed.estimate.usd
66
+ : undefined;
67
+ if (usd === undefined) {
68
+ return {
69
+ available: false,
70
+ warnings,
71
+ reason: classifyUnavailable(warnings, usd),
72
+ };
73
+ }
74
+ return assessComputedEstimate({ usd, warnings });
75
+ }
76
+ catch {
77
+ return {
78
+ available: false,
79
+ warnings: [],
80
+ reason: "estimator-threw",
81
+ };
82
+ }
83
+ }
84
+ export function sanitizedCostEstimateLog(input) {
85
+ return diagnosticRecord("generation-cost-estimate", {
86
+ ...input.correlation,
87
+ available: input.assessment.available,
88
+ ...(input.assessment.available ? {} : { reason: input.assessment.reason }),
89
+ warningCount: input.assessment.warnings.length,
90
+ });
91
+ }
92
+ export async function logUnavailableCostEstimate(input) {
93
+ if (input.assessment.available)
94
+ return;
95
+ const line = sanitizedCostEstimateLog(input);
96
+ logger.warn("generation-cost-estimate", line);
97
+ if (!input.engine)
98
+ return;
99
+ await appendDiagnostic(input.engine, "generation-cost-estimate", line).catch(() => undefined);
100
+ }
101
+ function classifyUnavailable(warnings, usd) {
102
+ const text = warnings.join(" ").toLowerCase();
103
+ if (/no cost adapter/.test(text))
104
+ return "missing-adapter";
105
+ if (/not found in pricing table/.test(text) || /model '.+' not found/.test(text)) {
106
+ return "missing-rates";
107
+ }
108
+ if (/selector|variant '/.test(text))
109
+ return "missing-selector";
110
+ if (usd === 0)
111
+ return "zero";
112
+ return "unbounded";
113
+ }
@@ -0,0 +1,47 @@
1
+ import { registerJobHandler, queue } from "../queue.js";
2
+ import { cropRegion, probeImage } from "../image.js";
3
+ import { withLockedLocalLease } from "./_lease.js";
4
+ import { cropGeometry } from "../crop-geometry.js";
5
+ const LOCK_DURATION_MS = 300_000;
6
+ export function registerCropImageJob(engine) {
7
+ registerJobHandler("crop_image", async (payload, ctx) => {
8
+ const { inputFilename, ratio, rect, assetId, assetDir, bookId, } = payload;
9
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "crop", { durationMs: LOCK_DURATION_MS, data: { state: "cropping", ratio } }, async () => {
10
+ const readResult = await engine.files.read(assetId, inputFilename);
11
+ if (!readResult.ok) {
12
+ throw new Error(`Failed to read ${inputFilename}: ${readResult.error.message}`);
13
+ }
14
+ const buffer = Buffer.isBuffer(readResult.value)
15
+ ? readResult.value
16
+ : Buffer.from(readResult.value);
17
+ const src = await probeImage(buffer);
18
+ if (!src.width || !src.height) {
19
+ throw new Error(`Could not probe source dimensions for ${inputFilename}`);
20
+ }
21
+ const { outW, outH } = cropGeometry(rect, src.width, src.height, ratio, 1);
22
+ const cropped = await cropRegion(buffer, rect, outW, outH);
23
+ const out = await probeImage(cropped);
24
+ if (out.width !== outW || out.height !== outH) {
25
+ throw new Error(`Crop output dimensions ${out.width}x${out.height} do not match canonical ${outW}x${outH}`);
26
+ }
27
+ const finalFilename = "original.jpg";
28
+ const writeResult = await engine.files.write(assetId, finalFilename, cropped);
29
+ if (!writeResult.ok) {
30
+ throw new Error(`Failed to write cropped image: ${writeResult.error.message}`);
31
+ }
32
+ // Drop the previous original when it was a different filename (e.g. original.png).
33
+ if (inputFilename !== finalFilename) {
34
+ await engine.files.delete(assetId, inputFilename);
35
+ }
36
+ return { extension: ".jpg" };
37
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
38
+ await queue.enqueueAndWait(`process-image-${assetId}-${Date.now()}`, {
39
+ type: "process_image",
40
+ assetId,
41
+ bookId,
42
+ extension: result.extension,
43
+ provenance: { origin: "crop", ratio, cropped_at: new Date().toISOString() },
44
+ });
45
+ return { success: true };
46
+ });
47
+ }
@@ -0,0 +1,71 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { registerJobHandler, queue } from "../queue.js";
4
+ import { runFfmpeg, probeVideo, H264_MEDIUM, AUDIO_OPTS } from "../ffmpeg.js";
5
+ import { withLockedLocalLease } from "./_lease.js";
6
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
7
+ import { cropGeometry } from "../crop-geometry.js";
8
+ const LOCK_DURATION_MS = 300_000;
9
+ export function registerCropVideoJob(engine) {
10
+ registerJobHandler("crop_video", async (payload, ctx) => {
11
+ const { inputPath, outputPath, ratio, rect, assetId, assetDir, bookId, } = payload;
12
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "crop", { durationMs: LOCK_DURATION_MS, data: { state: "cropping", ratio } }, async () => {
13
+ const src = await probeVideo(inputPath);
14
+ if (!src.width || !src.height) {
15
+ throw new Error(`Could not probe source dimensions for ${inputPath}`);
16
+ }
17
+ const { cropX, cropY, cropW, cropH, outW, outH } = cropGeometry(rect, src.width, src.height, ratio, 2);
18
+ const vf = `crop=${cropW}:${cropH}:${cropX}:${cropY},` +
19
+ `scale=${outW}:${outH}:flags=lanczos,setsar=1`;
20
+ const args = [
21
+ "-y", "-i", inputPath,
22
+ "-vf", vf,
23
+ ...H264_MEDIUM,
24
+ ...(src.hasAudio ? AUDIO_OPTS : ["-an"]),
25
+ "-movflags", "+faststart",
26
+ outputPath,
27
+ ];
28
+ try {
29
+ await runFfmpeg(args);
30
+ }
31
+ catch (err) {
32
+ await rm(outputPath, { force: true });
33
+ throw err;
34
+ }
35
+ try {
36
+ const out = await probeVideo(outputPath);
37
+ if (out.width !== outW || out.height !== outH) {
38
+ throw new Error(`Crop output dimensions ${out.width}x${out.height} do not match canonical ${outW}x${outH}`);
39
+ }
40
+ if (!(out.duration > 0)) {
41
+ throw new Error(`Crop output has zero duration`);
42
+ }
43
+ }
44
+ catch (err) {
45
+ await rm(outputPath, { force: true });
46
+ throw err;
47
+ }
48
+ const inputFilename = basename(inputPath);
49
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
50
+ for (const stale of [
51
+ ".original.analysis.json",
52
+ ".analysis.gpt4otrans.json",
53
+ "elevenlabs.json",
54
+ ]) {
55
+ await engine.files.delete(assetId, stale);
56
+ }
57
+ const videoPath = join(assetDir, inputFilename);
58
+ return { videoPath };
59
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
60
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
61
+ type: "process_video",
62
+ videoPath: result.videoPath,
63
+ assetId,
64
+ assetDir,
65
+ bookId,
66
+ provenance: { origin: "crop", ratio, cropped_at: new Date().toISOString() },
67
+ force: true,
68
+ });
69
+ return { success: true };
70
+ });
71
+ }
@@ -0,0 +1,181 @@
1
+ import { join, dirname, extname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readFile } from "node:fs/promises";
4
+ import { logger } from "../logger.js";
5
+ import { registerJobHandler } from "../queue.js";
6
+ import { MAX_AUTOMATIC_TAGS, frameVocabularyFor } from "../tag-vocabulary.js";
7
+ import { requireApiKey } from "../api-key.js";
8
+ import { withLocalLease } from "./_lease.js";
9
+ import { enqueueAssetTagging } from "./tag-asset.js";
10
+ import { INTELLIGENCE_MODEL, buildIntelligenceResponses, firstResponseText, intelligenceRequest, } from "../openai-intelligence.js";
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const GUIDE_PATH = join(__dirname, "../guides/image_reproduction_guide.md");
13
+ export const SCRIPT_FILENAME = "original.md";
14
+ export const ANALYSIS_FILENAME = ".original.analysis.json";
15
+ const VISION_MODEL = INTELLIGENCE_MODEL;
16
+ export const PROMPT_CHAR_LIMIT = 1900;
17
+ const DESCRIBE_IMAGE_SCHEMA = {
18
+ type: "object",
19
+ additionalProperties: false,
20
+ properties: {
21
+ prompt: { type: "string", maxLength: PROMPT_CHAR_LIMIT },
22
+ shot: { type: "string" },
23
+ pose: { type: "string" },
24
+ tags: {
25
+ type: "array",
26
+ maxItems: MAX_AUTOMATIC_TAGS,
27
+ items: {
28
+ type: "object",
29
+ additionalProperties: false,
30
+ properties: {
31
+ facet: { type: "string", enum: ["people", "places", "editing"] },
32
+ label: { type: "string" },
33
+ },
34
+ required: ["facet", "label"],
35
+ },
36
+ },
37
+ },
38
+ required: ["prompt", "shot", "pose", "tags"],
39
+ };
40
+ /**
41
+ * The tagging half of the instructions. The prompt half describes how to
42
+ * reproduce the image; this half describes how to file it. Offering the
43
+ * vocabulary inline is what makes separate clips converge on one spelling —
44
+ * anything outside it is discarded on the way in, so asking for invented
45
+ * terms would only waste tokens.
46
+ */
47
+ function buildTaggingPrompt() {
48
+ return ("\n\nTAGS: also return a \"tags\" array of {facet, label} records that help an editor "
49
+ + "find and group this image with related footage. Emit a tag only when the image itself "
50
+ + "supports it, prefer few precise tags over many, and return an empty array when nothing "
51
+ + "is worth grouping by. Never tag how good or how moody the image looks, and never name a "
52
+ + "real person or a real place.\n"
53
+ + `- facet "people" — one of: ${frameVocabularyFor("people").join(", ")}.\n`
54
+ + `- facet "places" — the setting, plus interior/exterior and day/night when visible. `
55
+ + `Common settings: ${frameVocabularyFor("places").join(", ")}. A concrete unlisted setting is fine.\n`
56
+ + `- facet "editing" — framing and coverage only: ${frameVocabularyFor("editing").join(", ")}. `
57
+ + "A still image cannot show camera movement, so never claim pan, tilt, tracking or handheld.");
58
+ }
59
+ function mimeForFilename(filename) {
60
+ const ext = extname(filename).replace(/^\./, "").toLowerCase();
61
+ if (ext === "png")
62
+ return "image/png";
63
+ if (ext === "webp")
64
+ return "image/webp";
65
+ return "image/jpeg";
66
+ }
67
+ function buildSystemPrompt(guide) {
68
+ return ("You are an expert image-to-prompt analyst. You look at an image and emit a reproduction-ready prompt " +
69
+ "for an image generator. Follow the guide below exactly. Return ONLY a JSON object matching the schema " +
70
+ "described in the guide. Do not wrap it in prose or markdown fences.\n\n" +
71
+ `HARD LIMIT: the "prompt" field MUST be ${PROMPT_CHAR_LIMIT} characters or fewer. Trim detail to fit; never exceed.\n\n` +
72
+ "REGISTER: If the image reads as a casual phone/social snapshot (candid framing, available light, " +
73
+ "unretouched skin, compression artifacts), use the Authentic-photo register from the guide. Do not " +
74
+ "inject editorial film-stock, 'cinematic', 'masterpiece', or pro-film vocabulary into snapshots. " +
75
+ "Default to authentic phone capture when in doubt.\n\n" +
76
+ `--- REPRODUCTION GUIDE ---\n${guide}\n--- END GUIDE ---`
77
+ + buildTaggingPrompt());
78
+ }
79
+ export function formatMarkdown(r) {
80
+ const lines = [];
81
+ lines.push(r.prompt.trim(), "");
82
+ const rows = [
83
+ ["Shot", r.shot],
84
+ ["Pose", r.pose],
85
+ ];
86
+ for (const [label, value] of rows) {
87
+ if (value && value.trim()) {
88
+ lines.push(`${label}: ${value.trim()}`);
89
+ }
90
+ }
91
+ lines.push("");
92
+ return lines.join("\n");
93
+ }
94
+ export function mimeForImageFilename(filename) {
95
+ return mimeForFilename(filename);
96
+ }
97
+ function buildResponses(apiKey) {
98
+ return buildIntelligenceResponses(apiKey);
99
+ }
100
+ export async function describeImageBytes(bytes, mime, responses) {
101
+ const fn = responses ?? buildResponses(requireApiKey("OPENAI_API_KEY"));
102
+ const guide = await readFile(GUIDE_PATH, "utf-8");
103
+ const dataUrl = `data:${mime};base64,${bytes.toString("base64")}`;
104
+ const response = await fn(intelligenceRequest({
105
+ instructions: buildSystemPrompt(guide),
106
+ input: [{
107
+ role: "user",
108
+ content: [
109
+ { type: "input_image", image_url: dataUrl, detail: "high" },
110
+ { type: "input_text", text: "Analyze this image and produce the reproduction prompt as a JSON object matching the schema in the guide." },
111
+ ],
112
+ }],
113
+ text: {
114
+ format: {
115
+ type: "json_schema",
116
+ name: "image_reproduction",
117
+ schema: DESCRIBE_IMAGE_SCHEMA,
118
+ strict: true,
119
+ },
120
+ },
121
+ max_output_tokens: 2048,
122
+ }));
123
+ const content = firstResponseText(response);
124
+ if (!content) {
125
+ throw new Error(`Vision provider returned no content (status=${response.status})`);
126
+ }
127
+ const parsed = JSON.parse(content);
128
+ if (!parsed.prompt || typeof parsed.prompt !== "string") {
129
+ throw new Error(`Vision model returned no prompt field. Got keys: ${Object.keys(parsed).join(",")}`);
130
+ }
131
+ if (parsed.prompt.length > PROMPT_CHAR_LIMIT) {
132
+ logger.warn("description-truncated", { count: parsed.prompt.length, limit: PROMPT_CHAR_LIMIT });
133
+ parsed.prompt = parsed.prompt.slice(0, PROMPT_CHAR_LIMIT).trimEnd();
134
+ }
135
+ return parsed;
136
+ }
137
+ export function registerDescribeImageJob(engine) {
138
+ registerJobHandler("describe_image", async (payload, ctx) => {
139
+ const { sourceAssetId, sourceImageFilename, assetId: scriptAssetId, } = payload;
140
+ // When scriptAssetId is set, the queue wrote a pending row on it; claim
141
+ // the lease so the wrapper finalizes it (ready/error). Analysis-only
142
+ // mode runs on the source asset, which already exists at status='ready'.
143
+ const targetAssetId = scriptAssetId ?? sourceAssetId;
144
+ return withLocalLease(engine, targetAssetId, ctx, "describe", async () => {
145
+ const responses = buildResponses(requireApiKey("OPENAI_API_KEY"));
146
+ const readResult = await engine.files.read(sourceAssetId, sourceImageFilename);
147
+ if (!readResult.ok) {
148
+ throw new Error(`Failed to read image ${sourceImageFilename} for ${sourceAssetId}: ${readResult.error.message}`);
149
+ }
150
+ const buffer = Buffer.from(readResult.value);
151
+ const mime = mimeForFilename(sourceImageFilename);
152
+ const parsed = await describeImageBytes(buffer, mime, responses);
153
+ const analysisJson = JSON.stringify(parsed, null, 2);
154
+ const cacheResult = await engine.files.write(sourceAssetId, ANALYSIS_FILENAME, analysisJson);
155
+ if (!cacheResult.ok) {
156
+ if (cacheResult.error.code === "NOT_FOUND") {
157
+ return { success: false, sourceAssetId, scriptAssetId, reason: "source_deleted" };
158
+ }
159
+ logger.warn("description-cache-failed", { assetId: sourceAssetId, error: cacheResult.error });
160
+ }
161
+ // The structured tags rode along with the prompt, so tagging is now a
162
+ // read of what this request already returned.
163
+ await enqueueAssetTagging(engine, sourceAssetId);
164
+ if (scriptAssetId) {
165
+ const markdown = formatMarkdown(parsed);
166
+ const writeResult = await engine.files.write(scriptAssetId, SCRIPT_FILENAME, markdown);
167
+ if (!writeResult.ok) {
168
+ throw new Error(`Failed to write ${SCRIPT_FILENAME} to ${scriptAssetId}: ${writeResult.error.message}`);
169
+ }
170
+ await engine.metadata.artifacts.write(scriptAssetId, "original", {
171
+ origin: "describe_image",
172
+ sourceAssetId,
173
+ model: VISION_MODEL,
174
+ processed_at: new Date().toISOString(),
175
+ variant: "full",
176
+ });
177
+ }
178
+ return { success: true, sourceAssetId, scriptAssetId, prompt: parsed.prompt };
179
+ });
180
+ });
181
+ }
@@ -0,0 +1,33 @@
1
+ import { findVideoFile } from "videobook-engine";
2
+ import { join } from "node:path";
3
+ import { registerJobHandler } from "../queue.js";
4
+ import { withLocalLease } from "./_lease.js";
5
+ import { detectSceneCandidates } from "../scene-detect.js";
6
+ export function registerDetectScenesJob(engine) {
7
+ registerJobHandler("detect_scenes", async (payload, ctx) => {
8
+ const { assetId } = payload;
9
+ return withLocalLease(engine, assetId, ctx, "analyze", async () => {
10
+ const manifestResult = await engine.files.manifest(assetId);
11
+ if (!manifestResult.ok)
12
+ throw new Error(manifestResult.error.message);
13
+ const videoFile = findVideoFile(manifestResult.value.files);
14
+ if (!videoFile)
15
+ throw new Error(`No video file found in asset ${assetId}`);
16
+ const candidates = await detectSceneCandidates(join(manifestResult.value.path, videoFile.name));
17
+ const shots = candidates.filter((c) => c.kind === "shot").length;
18
+ const settles = candidates.filter((c) => c.kind === "settle").length;
19
+ const final = {
20
+ asset_id: assetId,
21
+ count: candidates.length,
22
+ shots,
23
+ settles,
24
+ candidates,
25
+ };
26
+ const writeResult = await engine.files.write(assetId, ".detect-scenes.json", JSON.stringify(final, null, 2));
27
+ if (!writeResult.ok) {
28
+ throw new Error(`Scenes detected but failed to persist: ${writeResult.error.message}`);
29
+ }
30
+ return { success: true, count: candidates.length, shots, settles };
31
+ });
32
+ });
33
+ }