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,283 @@
1
+ import { join } from "node:path";
2
+ import { mkdir, mkdtemp, writeFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { logger } from "../logger.js";
5
+ import { registerJobHandler } from "../queue.js";
6
+ import { runFfmpeg, probeVideo, AUDIO_OPTS, H264_MEDIUM } from "../ffmpeg.js";
7
+ import { withLockedLocalLease } from "./_lease.js";
8
+ import { ingestWorkspaceOutputs } from "./_persist-output.js";
9
+ const LOCK_DURATION_MS = 1_800_000;
10
+ const LANDSCAPE_RENDER = "landscape.mp4";
11
+ const PORTRAIT_RENDER = "portrait.mp4";
12
+ const SQUARE_RENDER = "square.mp4";
13
+ const LANDSCAPE_OUTPUT = "timeline_landscape.mp4";
14
+ const PORTRAIT_OUTPUT = "timeline_portrait.mp4";
15
+ const SQUARE_OUTPUT = "timeline_square.mp4";
16
+ export function buildAudioFilter(index, slot, duration, hasAudio = true) {
17
+ const filters = ["aresample=48000", "aformat=sample_fmts=fltp:channel_layouts=stereo"];
18
+ const vol = slot.volume / 100;
19
+ if (vol !== 1) {
20
+ filters.push(`volume=${vol}`);
21
+ }
22
+ const fadeInSec = slot.fadeIn / 1000;
23
+ if (fadeInSec > 0) {
24
+ filters.push(`afade=t=in:d=${fadeInSec}`);
25
+ }
26
+ const fadeOutSec = slot.fadeOut / 1000;
27
+ if (fadeOutSec > 0) {
28
+ const startTime = Math.max(0, duration - fadeOutSec);
29
+ filters.push(`afade=t=out:st=${startTime}:d=${fadeOutSec}`);
30
+ }
31
+ if (hasAudio) {
32
+ return `[${index}:a:0]${filters.join(",")}[a${index}]`;
33
+ }
34
+ return `anullsrc=channel_layout=stereo:sample_rate=48000,atrim=0:${duration},asetpts=PTS-STARTPTS,${filters.join(",")}[a${index}]`;
35
+ }
36
+ // A CFR file's measured average rate matches its nominal rate almost exactly;
37
+ // anything beyond this gap means uneven frame timestamps (VFR).
38
+ const VFR_TOLERANCE_FPS = 0.1;
39
+ const FALLBACK_FPS = 30;
40
+ /**
41
+ * Decide whether the timeline concat can stream-copy video. Stream-copying
42
+ * splices packets without touching timestamps, so it is only safe when every
43
+ * input is h264 at the same resolution and the same constant frame rate.
44
+ * Anything else (mixed fps across slots, a VFR input, a stray codec) must be
45
+ * re-encoded to one constant frame rate — otherwise the concatenated output
46
+ * carries uneven timestamps that turn into visible skipping once platforms
47
+ * like X transcode it assuming CFR.
48
+ */
49
+ export function planTimelineVideo(probes) {
50
+ const first = probes[0];
51
+ const uniformFps = probes.every((p) => Math.round(p.fps) === Math.round(first.fps));
52
+ const reasons = [];
53
+ if (probes.some((p) => p.videoCodec !== "h264"))
54
+ reasons.push("non-h264 input");
55
+ if (!uniformFps)
56
+ reasons.push("mixed frame rates");
57
+ if (probes.some((p) => p.width !== first.width || p.height !== first.height)) {
58
+ reasons.push("mixed resolutions");
59
+ }
60
+ if (probes.some((p) => p.avgFps !== null && Math.abs(p.avgFps - p.fps) > VFR_TOLERANCE_FPS)) {
61
+ reasons.push("variable frame rate input");
62
+ }
63
+ if (reasons.length === 0)
64
+ return { mode: "copy" };
65
+ return {
66
+ mode: "reencode",
67
+ targetFps: uniformFps ? Math.round(first.fps) || FALLBACK_FPS : FALLBACK_FPS,
68
+ width: first.width,
69
+ height: first.height,
70
+ reasons,
71
+ };
72
+ }
73
+ function buildReencodeVideoFilter(index, plan) {
74
+ const { targetFps, width, height } = plan;
75
+ return (`[${index}:v:0]fps=${targetFps},` +
76
+ `scale=${width}:${height}:force_original_aspect_ratio=decrease,` +
77
+ `pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1,format=yuv420p[v${index}]`);
78
+ }
79
+ /**
80
+ * ffmpeg args for the re-encode path: each slot is its own input, video is
81
+ * normalized to one CFR rate/resolution and concatenated via the concat
82
+ * filter alongside the per-slot audio chains.
83
+ */
84
+ export function buildReencodeArgs(opts) {
85
+ const { inputPaths, resolvedSlots, durations, hasAudioFlags, resolvedAudio, plan, outputPath } = opts;
86
+ const n = inputPaths.length;
87
+ const hasOverlays = resolvedAudio.length > 0;
88
+ const args = [];
89
+ for (const p of inputPaths)
90
+ args.push("-i", p);
91
+ for (const overlay of resolvedAudio)
92
+ args.push("-i", overlay.audioPath);
93
+ const filterParts = [];
94
+ for (let i = 0; i < n; i++) {
95
+ filterParts.push(buildReencodeVideoFilter(i, plan));
96
+ filterParts.push(buildAudioFilter(i, resolvedSlots[i], durations[i], hasAudioFlags[i]));
97
+ }
98
+ const concatInputs = Array.from({ length: n }, (_, i) => `[v${i}][a${i}]`).join("");
99
+ const concatAudioLabel = hasOverlays ? "[concatA]" : "[outa]";
100
+ filterParts.push(`${concatInputs}concat=n=${n}:v=1:a=1[outv]${concatAudioLabel}`);
101
+ if (hasOverlays) {
102
+ const overlayLabels = [];
103
+ resolvedAudio.forEach((overlay, k) => {
104
+ const label = `ov${k}`;
105
+ overlayLabels.push(`[${label}]`);
106
+ filterParts.push(buildOverlayAudioFilter(n + k, overlay, label));
107
+ });
108
+ const mixCount = 1 + overlayLabels.length;
109
+ filterParts.push(`[concatA]${overlayLabels.join("")}amix=inputs=${mixCount}:duration=first:dropout_transition=0:normalize=0,aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo[outa]`);
110
+ }
111
+ args.push("-filter_complex", filterParts.join(";"), "-map", "[outv]", ...H264_MEDIUM, "-pix_fmt", "yuv420p", "-map", "[outa]", ...AUDIO_OPTS, "-movflags", "+faststart", "-y", outputPath);
112
+ return args;
113
+ }
114
+ function buildOverlayAudioFilter(inputIndex, overlay, label) {
115
+ const filters = ["aresample=48000", "aformat=sample_fmts=fltp:channel_layouts=stereo"];
116
+ const durationSec = overlay.durationMs / 1000;
117
+ if (durationSec > 0) {
118
+ filters.push(`atrim=0:${durationSec}`);
119
+ }
120
+ if (overlay.startMs > 0) {
121
+ filters.push(`adelay=${overlay.startMs}|${overlay.startMs}`);
122
+ }
123
+ if (overlay.volume !== 1) {
124
+ filters.push(`volume=${overlay.volume}`);
125
+ }
126
+ const fadeInSec = overlay.fadeInMs / 1000;
127
+ if (fadeInSec > 0) {
128
+ filters.push(`afade=t=in:st=${overlay.startMs / 1000}:d=${fadeInSec}`);
129
+ }
130
+ const fadeOutSec = overlay.fadeOutMs / 1000;
131
+ if (fadeOutSec > 0) {
132
+ const fadeOutStart = Math.max(0, (overlay.startMs + overlay.durationMs) / 1000 - fadeOutSec);
133
+ filters.push(`afade=t=out:st=${fadeOutStart}:d=${fadeOutSec}`);
134
+ }
135
+ return `[${inputIndex}:a:0]${filters.join(",")}[${label}]`;
136
+ }
137
+ /**
138
+ * Fast path: video stream-copied via the concat demuxer. Only valid when
139
+ * planTimelineVideo() returned "copy" (uniform CFR h264 inputs). Audio is
140
+ * stream-copied too unless a slot deviates from default volume/fades or
141
+ * overlay clips must be mixed in.
142
+ */
143
+ async function runCopyConcat(opts) {
144
+ const { inputPaths, resolvedSlots, durations, hasAudioFlags, resolvedAudio, outputPath, signal } = opts;
145
+ const n = resolvedSlots.length;
146
+ const needsSlotAudioFilter = resolvedSlots.some((s) => s.volume !== 100 || s.fadeIn > 0 || s.fadeOut > 0);
147
+ const hasOverlays = resolvedAudio.length > 0;
148
+ const needsAudioFilter = needsSlotAudioFilter || hasOverlays;
149
+ const concatListDir = await mkdtemp(join(tmpdir(), "videocity-concat-"));
150
+ const concatListPath = join(concatListDir, "list.txt");
151
+ try {
152
+ // concat demuxer list: one `file '<path>'` per line, single-quotes
153
+ // escaped by `'\''`.
154
+ const listBody = inputPaths
155
+ .map((p) => `file '${p.replace(/'/g, "'\\''")}'`)
156
+ .join("\n");
157
+ await writeFile(concatListPath, listBody + "\n", "utf8");
158
+ const ffmpegArgs = ["-f", "concat", "-safe", "0", "-i", concatListPath];
159
+ if (needsAudioFilter) {
160
+ // Re-open each slot as its own input so we can apply per-slot audio
161
+ // filters, then concat the audio streams. Video comes from input 0
162
+ // (the concat demuxer) and is stream-copied.
163
+ for (const p of inputPaths) {
164
+ ffmpegArgs.push("-i", p);
165
+ }
166
+ for (const overlay of resolvedAudio) {
167
+ ffmpegArgs.push("-i", overlay.audioPath);
168
+ }
169
+ const filterParts = [];
170
+ for (let i = 0; i < n; i++) {
171
+ // Input indices for the per-slot reopens start at 1.
172
+ filterParts.push(buildAudioFilter(i + 1, resolvedSlots[i], durations[i], hasAudioFlags[i]));
173
+ }
174
+ const concatInputs = Array.from({ length: n }, (_, i) => `[a${i + 1}]`).join("");
175
+ const concatLabel = hasOverlays ? "[concatA]" : "[outa]";
176
+ filterParts.push(`${concatInputs}concat=n=${n}:v=0:a=1${concatLabel}`);
177
+ if (hasOverlays) {
178
+ const overlayBaseIndex = 1 + n;
179
+ const overlayLabels = [];
180
+ resolvedAudio.forEach((overlay, k) => {
181
+ const label = `ov${k}`;
182
+ overlayLabels.push(`[${label}]`);
183
+ filterParts.push(buildOverlayAudioFilter(overlayBaseIndex + k, overlay, label));
184
+ });
185
+ const mixInputs = `[concatA]${overlayLabels.join("")}`;
186
+ const mixCount = 1 + overlayLabels.length;
187
+ filterParts.push(`${mixInputs}amix=inputs=${mixCount}:duration=first:dropout_transition=0:normalize=0,aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo[outa]`);
188
+ }
189
+ ffmpegArgs.push("-filter_complex", filterParts.join(";"), "-map", "0:v:0", "-c:v", "copy", "-map", "[outa]", ...AUDIO_OPTS);
190
+ }
191
+ else {
192
+ // No audio adjustments: stream-copy both tracks.
193
+ ffmpegArgs.push("-map", "0:v:0", "-map", "0:a:0?", "-c", "copy");
194
+ }
195
+ ffmpegArgs.push("-movflags", "+faststart", "-y", outputPath);
196
+ await runFfmpeg(ffmpegArgs, { signal });
197
+ }
198
+ finally {
199
+ await rm(concatListDir, { recursive: true, force: true });
200
+ }
201
+ }
202
+ export function registerRenderTimelineJob(engine) {
203
+ registerJobHandler("render_timeline", async (payload, ctx) => {
204
+ ctx.signal.throwIfAborted();
205
+ const { bookId, orientation, resolvedSlots, resolvedAudio = [], } = payload;
206
+ const existingFinal = engine.artifacts.list().find((artifact) => artifact.kind === "final" && artifact.label === "final");
207
+ const finalResult = existingFinal !== undefined
208
+ ? { ok: true, value: existingFinal }
209
+ : await engine.artifacts.create({ kind: "final", label: "final" });
210
+ if (!finalResult.ok)
211
+ throw new Error(finalResult.error.message);
212
+ const finalId = finalResult.value.artifactId;
213
+ const finalDir = finalResult.value.path;
214
+ await mkdir(finalDir, { recursive: true });
215
+ return withLockedLocalLease(engine, finalId, finalDir, ctx, "final", { durationMs: LOCK_DURATION_MS }, async () => {
216
+ const renderFilename = orientation === "portrait" ? PORTRAIT_RENDER : orientation === "square" ? SQUARE_RENDER : LANDSCAPE_RENDER;
217
+ const outputFilename = orientation === "portrait" ? PORTRAIT_OUTPUT : orientation === "square" ? SQUARE_OUTPUT : LANDSCAPE_OUTPUT;
218
+ const outputPath = join(finalDir, outputFilename);
219
+ const inputPaths = [];
220
+ const durations = [];
221
+ const hasAudioFlags = [];
222
+ const videoProbes = [];
223
+ for (const slot of resolvedSlots) {
224
+ ctx.signal.throwIfAborted();
225
+ const inputPath = join(slot.assetDir, renderFilename);
226
+ inputPaths.push(inputPath);
227
+ const probe = await probeVideo(inputPath, { signal: ctx.signal });
228
+ durations.push(probe.duration);
229
+ hasAudioFlags.push(probe.hasAudio);
230
+ videoProbes.push({
231
+ fps: probe.fps,
232
+ avgFps: probe.avgFps,
233
+ width: probe.width,
234
+ height: probe.height,
235
+ videoCodec: probe.videoCodec,
236
+ });
237
+ }
238
+ const n = resolvedSlots.length;
239
+ // Slot renders normally share codec/resolution/fps (same Remotion
240
+ // pipeline), in which case video is stream-copied via the concat
241
+ // demuxer. But /render keeps each source clip's native frame rate, so
242
+ // a timeline mixing e.g. 24fps generated clips with 30fps footage —
243
+ // or containing any VFR input — must be re-encoded to one constant
244
+ // frame rate, or the spliced timestamps stutter after platforms
245
+ // (X, etc.) transcode the upload assuming CFR.
246
+ const plan = planTimelineVideo(videoProbes);
247
+ if (plan.mode === "reencode") {
248
+ logger.warn("timeline-reencode", { bookId, targetFps: plan.targetFps, count: plan.reasons.length });
249
+ await runFfmpeg(buildReencodeArgs({
250
+ inputPaths,
251
+ resolvedSlots,
252
+ durations,
253
+ hasAudioFlags,
254
+ resolvedAudio,
255
+ plan,
256
+ outputPath,
257
+ }), { signal: ctx.signal });
258
+ }
259
+ else {
260
+ await runCopyConcat({
261
+ inputPaths,
262
+ resolvedSlots,
263
+ durations,
264
+ hasAudioFlags,
265
+ resolvedAudio,
266
+ outputPath,
267
+ signal: ctx.signal,
268
+ });
269
+ }
270
+ const thumbPath = join(finalDir, "original.jpg");
271
+ await runFfmpeg(["-y", "-i", outputPath, "-frames:v", "1", "-q:v", "2", thumbPath], { signal: ctx.signal });
272
+ await ingestWorkspaceOutputs(engine, finalId, [outputFilename, "original.jpg"], "render_timeline", { orientation, slotCount: n });
273
+ const renderMeta = {
274
+ slotCount: n,
275
+ slots: resolvedSlots.map((s) => s.artifactId),
276
+ output: outputFilename,
277
+ renderedAt: new Date().toISOString(),
278
+ };
279
+ await engine.metadata.artifacts.write(finalId, `${orientation}.render`, renderMeta);
280
+ return { success: true, output: outputFilename };
281
+ }, { durationMs: LOCK_DURATION_MS });
282
+ });
283
+ }
@@ -0,0 +1,42 @@
1
+ import { buildAssetList } from "../tools/reconcile-project-work.js";
2
+ import { missingVisualIndexes } from "../search-index-coverage.js";
3
+ import { registerLocalClipSearchProvider } from "../local-search-models.js";
4
+ import { registerJobHandler } from "../queue.js";
5
+ import { runSemanticIndex } from "./semantic-index.js";
6
+ export function registerRepairSearchIndexesJob(engine) {
7
+ registerJobHandler("repair_search_indexes", (payload, context) => repairSearchIndexes(engine, Array.isArray(payload.kinds) ? payload.kinds : ["image", "video"], context));
8
+ }
9
+ export function repairSearchIndexes(engine, requestedKinds, context) {
10
+ return engine.withLocalMedia(async () => {
11
+ context.signal.throwIfAborted();
12
+ const kinds = new Set(requestedKinds);
13
+ const assets = (await buildAssetList(engine, engine.book.get().bookId, "newest"))
14
+ .filter((asset) => kinds.has(asset.type === "final" ? "video" : asset.type));
15
+ const missing = missingVisualIndexes(engine, assets);
16
+ const result = { indexed: 0, failed: 0, failures: [] };
17
+ if (missing.length === 0)
18
+ return result;
19
+ await registerLocalClipSearchProvider(engine).prepare({ signal: context.signal });
20
+ for (const asset of missing) {
21
+ context.signal.throwIfAborted();
22
+ try {
23
+ const indexed = await runSemanticIndex(engine, {
24
+ type: "semantic_index", assetId: asset.artifact_id, bookId: engine.book.get().bookId, prepareVisualMedia: true, visualOnly: true,
25
+ }, context);
26
+ if (indexed.semanticVisual !== "ready" || missingVisualIndexes(engine, [asset]).length > 0) {
27
+ throw new Error("Visual indexing is incomplete. Check the source media and prepare the local search models before retrying.");
28
+ }
29
+ result.indexed += 1;
30
+ }
31
+ catch (error) {
32
+ context.signal.throwIfAborted();
33
+ result.failed += 1;
34
+ if (result.failures.length < 10)
35
+ result.failures.push({
36
+ artifactId: asset.artifact_id, message: error instanceof Error ? error.message : String(error),
37
+ });
38
+ }
39
+ }
40
+ return result;
41
+ });
42
+ }
@@ -0,0 +1,64 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join, basename } from "node:path";
4
+ import { registerJobHandler, queue } from "../queue.js";
5
+ import { AUDIO_OPTS, probeVideo, runFfmpeg } from "../ffmpeg.js";
6
+ import { withLockedLocalLease } from "./_lease.js";
7
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
8
+ const LOCK_DURATION_MS = 300_000;
9
+ const STALE_ANALYSIS_FILES = [
10
+ ".original.analysis.json",
11
+ ".analysis.gpt4otrans.json",
12
+ "elevenlabs.json",
13
+ ];
14
+ export function registerReplaceVideoAudioJob(engine) {
15
+ registerJobHandler("replace_video_audio", async (payload, ctx) => {
16
+ const { inputPath, audioPath, assetId, assetDir, audioAssetId, bookId, extraProvenance, } = payload;
17
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "replace_audio", { durationMs: LOCK_DURATION_MS, data: { state: "replacing-audio" } }, async () => {
18
+ const inputFilename = basename(inputPath);
19
+ const source = await probeVideo(inputPath, { signal: ctx.signal });
20
+ if (!source.videoStream || source.videoDuration <= 0)
21
+ throw new Error("Video source has no playable video stream");
22
+ const workspace = await mkdtemp(join(tmpdir(), "videobook-replace-audio-"));
23
+ const outputPath = join(workspace, "output.mp4");
24
+ try {
25
+ await runFfmpeg([
26
+ "-y", "-i", inputPath, "-i", audioPath,
27
+ "-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy",
28
+ "-af", `apad,atrim=duration=${source.videoDuration},asetpts=PTS-STARTPTS`,
29
+ ...AUDIO_OPTS, "-movflags", "+faststart", outputPath,
30
+ ], { signal: ctx.signal });
31
+ const output = await probeVideo(outputPath, { signal: ctx.signal });
32
+ const tolerance = Math.max(1 / (source.fps || 30), 0.05);
33
+ if (!output.hasAudio || Math.abs(output.videoDuration - source.videoDuration) > tolerance
34
+ || Math.abs((output.audioDuration ?? output.duration) - source.videoDuration) > tolerance) {
35
+ throw new Error("Replacement audio output does not preserve the video duration");
36
+ }
37
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
38
+ }
39
+ finally {
40
+ await rm(workspace, { recursive: true, force: true });
41
+ }
42
+ for (const stale of STALE_ANALYSIS_FILES) {
43
+ await engine.files.delete(assetId, stale);
44
+ }
45
+ const videoPath = join(assetDir, inputFilename);
46
+ return { videoPath };
47
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
48
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
49
+ type: "process_video",
50
+ videoPath: result.videoPath,
51
+ assetId,
52
+ assetDir,
53
+ bookId,
54
+ provenance: {
55
+ ...(extraProvenance ?? {}),
56
+ origin: "replace_audio",
57
+ source_audio: audioAssetId,
58
+ replaced_at: new Date().toISOString(),
59
+ },
60
+ force: true,
61
+ });
62
+ return { success: true };
63
+ });
64
+ }
@@ -0,0 +1,53 @@
1
+ import { join } from "node:path";
2
+ import { getArchiveStore } from "../archive-store.js";
3
+ import { queue, registerJobHandler } from "../queue.js";
4
+ import { withLockedLocalLease } from "./_lease.js";
5
+ const LOCK_DURATION_MS = 10 * 60_000;
6
+ function provenance(payload) {
7
+ return {
8
+ origin: "archive",
9
+ archive_id: payload.archiveId,
10
+ title: payload.title ?? "",
11
+ topics: payload.topics ?? [],
12
+ people: payload.people ?? [],
13
+ restored_at: new Date().toISOString(),
14
+ };
15
+ }
16
+ export function registerRestoreArchiveClipJob(engine) {
17
+ registerJobHandler("restore_archive_clip", async (payload, ctx) => {
18
+ const p = payload;
19
+ return withLockedLocalLease(engine, p.assetId, p.assetDir, ctx, "download", { durationMs: LOCK_DURATION_MS, data: { state: "downloading" } }, async () => {
20
+ const store = getArchiveStore();
21
+ const body = await store.getBytes(p.mediaKey);
22
+ if (!body)
23
+ throw new Error(`Archive media not found: ${p.mediaKey}`);
24
+ const write = await engine.files.write(p.assetId, p.filename, Buffer.from(body));
25
+ if (!write.ok)
26
+ throw new Error(write.error.message);
27
+ const meta = provenance(p);
28
+ if (p.mediaKind === "video") {
29
+ const processJobId = `process-video-${p.assetId}-${Date.now()}`;
30
+ await queue.enqueue(processJobId, {
31
+ type: "process_video",
32
+ videoPath: join(p.assetDir, p.filename),
33
+ assetId: p.assetId,
34
+ assetDir: p.assetDir,
35
+ bookId: p.bookId,
36
+ provenance: meta,
37
+ });
38
+ return { success: true, archive_id: p.archiveId, asset_id: p.assetId, job_id: processJobId };
39
+ }
40
+ const processJobId = `process-audio-${p.assetId}-${Date.now()}`;
41
+ await queue.enqueue(processJobId, {
42
+ type: "process_audio",
43
+ audioPath: join(p.assetDir, p.filename),
44
+ sourceFilename: p.filename,
45
+ assetId: p.assetId,
46
+ assetDir: p.assetDir,
47
+ bookId: p.bookId,
48
+ provenance: { ...meta, content_type: p.contentType },
49
+ });
50
+ return { success: true, archive_id: p.archiveId, asset_id: p.assetId, job_id: processJobId };
51
+ }, { durationMs: LOCK_DURATION_MS });
52
+ });
53
+ }
@@ -0,0 +1,53 @@
1
+ import { basename, join } from "node:path";
2
+ import { registerJobHandler, queue } from "../queue.js";
3
+ import { getFFmpeggy, H264_MEDIUM, AUDIO_OPTS } from "../ffmpeg.js";
4
+ import { withLockedLocalLease } from "./_lease.js";
5
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
6
+ const LOCK_DURATION_MS = 300_000;
7
+ function buildReverseOutputOptions(hasAudio) {
8
+ const opts = ["-vf", "reverse"];
9
+ if (hasAudio) {
10
+ opts.push("-af", "areverse");
11
+ }
12
+ else {
13
+ opts.push("-an");
14
+ }
15
+ opts.push(...H264_MEDIUM, ...AUDIO_OPTS);
16
+ return opts;
17
+ }
18
+ export function registerReverseVideoJob(engine) {
19
+ registerJobHandler("reverse_video", async (payload, ctx) => {
20
+ const { inputPath, outputPath, hasAudio, assetId, assetDir, bookId, } = payload;
21
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "reverse", { durationMs: LOCK_DURATION_MS, data: { state: "reversing" } }, async () => {
22
+ const outputOptions = buildReverseOutputOptions(hasAudio);
23
+ const FFmpeggy = await getFFmpeggy();
24
+ const ff = new FFmpeggy({
25
+ input: inputPath,
26
+ output: outputPath,
27
+ outputOptions,
28
+ overwriteExisting: true,
29
+ });
30
+ await ff.run();
31
+ await ff.done();
32
+ const inputFilename = basename(inputPath);
33
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
34
+ const videoPath = join(assetDir, inputFilename);
35
+ return { videoPath };
36
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
37
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
38
+ type: "process_video",
39
+ videoPath: result.videoPath,
40
+ assetId,
41
+ assetDir,
42
+ bookId,
43
+ provenance: {
44
+ origin: "reverse",
45
+ source_asset_id: assetId,
46
+ reversed_at: new Date().toISOString(),
47
+ },
48
+ force: true,
49
+ skipDownstreamJobs: true,
50
+ });
51
+ return { success: true };
52
+ });
53
+ }
@@ -0,0 +1,173 @@
1
+ import { registerJobHandler } from "../queue.js";
2
+ import { requireApiKey } from "../api-key.js";
3
+ import { INTELLIGENCE_MODEL, buildIntelligenceResponses, firstResponseText, intelligenceRequest, } from "../openai-intelligence.js";
4
+ import { withLocalLease } from "./_lease.js";
5
+ import { formatMarkdown, PROMPT_CHAR_LIMIT, SCRIPT_FILENAME, } from "./describe-image.js";
6
+ import { platformGuidance } from "../platform-guidance.js";
7
+ const FIELD_LABEL_TO_KEY = {
8
+ Shot: "shot",
9
+ Pose: "pose",
10
+ };
11
+ const FULL_REWRITE_JSON_SCHEMA = {
12
+ type: "object",
13
+ required: ["prompt", "shot", "pose"],
14
+ additionalProperties: false,
15
+ properties: {
16
+ prompt: { type: "string" },
17
+ shot: { anyOf: [{ type: "string" }, { type: "null" }] },
18
+ pose: { anyOf: [{ type: "string" }, { type: "null" }] },
19
+ },
20
+ };
21
+ const TLDR_REWRITE_JSON_SCHEMA = {
22
+ type: "object",
23
+ required: ["prompt"],
24
+ additionalProperties: false,
25
+ properties: {
26
+ prompt: { type: "string" },
27
+ },
28
+ };
29
+ function parseMarkdownToResult(md) {
30
+ const lines = md.split("\n");
31
+ const promptLines = [];
32
+ let i = 0;
33
+ while (i < lines.length && lines[i].trim() !== "") {
34
+ promptLines.push(lines[i]);
35
+ i++;
36
+ }
37
+ const result = { prompt: promptLines.join("\n").trim() };
38
+ while (i < lines.length && lines[i].trim() === "")
39
+ i++;
40
+ for (; i < lines.length; i++) {
41
+ const line = lines[i];
42
+ const match = line.match(/^([A-Z][a-z]+):\s*(.+)$/);
43
+ if (!match)
44
+ continue;
45
+ const key = FIELD_LABEL_TO_KEY[match[1]];
46
+ if (key)
47
+ result[key] = match[2].trim();
48
+ }
49
+ return result;
50
+ }
51
+ function buildFullSystemPrompt(platform) {
52
+ const platformBlock = platform ? `\n\n--- PLATFORM ---\n${platformGuidance(platform)}\n--- END PLATFORM ---` : "";
53
+ return ("You rewrite image-reproduction prompt documents. The input describes one scene, split into a prose " +
54
+ "prompt paragraph and two labeled breakdown fields (Shot, Pose). You apply a user transformation and " +
55
+ "emit a rewritten document in the same JSON shape.\n\n" +
56
+ "Return ONLY a JSON object with keys: prompt (string, REQUIRED), shot (string or null), pose (string or " +
57
+ "null). Use null when an optional field is absent from the input. The shot field is shot size + camera " +
58
+ "angle in standard cinematography terms (e.g. 'medium close-up, eye-level'). The pose field describes " +
59
+ "body geometry only (weight, stance, rotation, limb positions); for close-ups, also describe implied " +
60
+ "off-frame posture in one trailing sentence. The pose field must not mention clothing, hair, face, or " +
61
+ "setting. No prose outside the JSON. No markdown fences.\n\n" +
62
+ `HARD LIMIT: the "prompt" field MUST be ${PROMPT_CHAR_LIMIT} characters or fewer.\n\n` +
63
+ "When a transformation instruction is provided, apply it rigorously: any subject being replaced must " +
64
+ "be ABSENT from every field of the output. Rewrite every field end-to-end so it describes the scene " +
65
+ "AFTER the transformation. Adapt subject, setting, and mood naturally to the new content; do NOT " +
66
+ "simply string-replace nouns.\n\n" +
67
+ "Preserve from the original: framing/angle/shot type, camera distance, lighting direction and time of " +
68
+ "day, overall capture register (authentic phone snapshot vs editorial), and aspect ratio cues." +
69
+ platformBlock);
70
+ }
71
+ function buildTldrSystemPrompt(platform) {
72
+ const platformBlock = platform ? `\n\n--- PLATFORM ---\n${platformGuidance(platform)}\n--- END PLATFORM ---` : "";
73
+ return ("You rewrite image-reproduction prompt documents into a 1–2 sentence TLDR. The input may be a full " +
74
+ "document (prose paragraph + labeled fields) or a TLDR already. You emit a tight 1–2 sentence prompt " +
75
+ "(roughly 40–70 words) capturing subject, setting, framing, light, and mood.\n\n" +
76
+ "Match the input's register: if it reads as a casual phone/social photo, include authentic-capture cues " +
77
+ "(mild grain, subtle sensor noise, tiny chromatic aberration, no beauty filter, unedited skin) and AVOID " +
78
+ "words like 'editorial', 'cinematic', 'masterpiece', or named film stocks (Portra, Cinestill, etc.). " +
79
+ "Only use film-stock / cinematic vocabulary if the input clearly references a pro editorial or film " +
80
+ "still. Never apply camera vocabulary to a painting or render.\n\n" +
81
+ "Return ONLY a JSON object: { \"prompt\": string }. No breakdown fields. No prose outside the JSON. " +
82
+ "No markdown fences.\n\n" +
83
+ "When a transformation instruction is provided, apply it rigorously: any subject being replaced must " +
84
+ "NOT appear in the output. Describe only the scene AFTER the transformation." +
85
+ platformBlock);
86
+ }
87
+ function buildUserMessage(current, customPrompt) {
88
+ const parts = [
89
+ "CURRENT DOCUMENT (JSON):",
90
+ JSON.stringify(current, null, 2),
91
+ ];
92
+ if (customPrompt && customPrompt.trim().length > 0) {
93
+ parts.push("", "TRANSFORMATION INSTRUCTION (apply this; any subject being replaced must vanish from every field):", customPrompt.trim());
94
+ }
95
+ else {
96
+ parts.push("", "No transformation instruction. Rewrite the document to match any new platform/variant guidance above while preserving the scene content.");
97
+ }
98
+ return parts.join("\n");
99
+ }
100
+ async function rewriteText(responses, current, opts, signal) {
101
+ const system = opts.tldr ? buildTldrSystemPrompt(opts.platform) : buildFullSystemPrompt(opts.platform);
102
+ const user = buildUserMessage(current, opts.customPrompt);
103
+ const response = await responses(intelligenceRequest({
104
+ instructions: system,
105
+ input: [{ role: "user", content: user }],
106
+ text: {
107
+ format: {
108
+ type: "json_schema",
109
+ name: opts.tldr ? "rewrite_script_tldr" : "rewrite_script_full",
110
+ schema: opts.tldr ? TLDR_REWRITE_JSON_SCHEMA : FULL_REWRITE_JSON_SCHEMA,
111
+ strict: true,
112
+ },
113
+ },
114
+ max_output_tokens: 2048,
115
+ }), signal);
116
+ const content = firstResponseText(response);
117
+ if (!content) {
118
+ throw new Error(`Rewrite provider returned no content (status=${response.status})`);
119
+ }
120
+ const raw = JSON.parse(content);
121
+ if (!raw.prompt || typeof raw.prompt !== "string") {
122
+ throw new Error(`Rewrite model returned no prompt field. Got keys: ${Object.keys(raw).join(",")}`);
123
+ }
124
+ const parsed = {
125
+ prompt: raw.prompt,
126
+ ...(typeof raw.shot === "string" ? { shot: raw.shot } : {}),
127
+ ...(typeof raw.pose === "string" ? { pose: raw.pose } : {}),
128
+ };
129
+ if (parsed.prompt.length > PROMPT_CHAR_LIMIT) {
130
+ parsed.prompt = parsed.prompt.slice(0, PROMPT_CHAR_LIMIT).trimEnd();
131
+ }
132
+ return parsed;
133
+ }
134
+ function formatTldrMarkdown(r) {
135
+ return `${r.prompt.trim()}\n`;
136
+ }
137
+ export function registerRewriteScriptJob(engine) {
138
+ registerJobHandler("rewrite_script", async (payload, ctx) => {
139
+ const { assetId: scriptAssetId, sourceAssetId, tldr, platform, customPrompt, } = payload;
140
+ return withLocalLease(engine, scriptAssetId, ctx, "rewrite_script", async () => {
141
+ const responses = buildIntelligenceResponses(requireApiKey("OPENAI_API_KEY"));
142
+ const readResult = await engine.files.read(scriptAssetId, SCRIPT_FILENAME);
143
+ if (!readResult.ok) {
144
+ throw new Error(`Failed to read ${SCRIPT_FILENAME} from ${scriptAssetId}: ${readResult.error.message}`);
145
+ }
146
+ const currentMarkdown = Buffer.from(readResult.value).toString("utf-8");
147
+ const current = parseMarkdownToResult(currentMarkdown);
148
+ if (!current.prompt) {
149
+ throw new Error(`Script ${scriptAssetId} has no parseable prompt in ${SCRIPT_FILENAME}`);
150
+ }
151
+ const rewritten = await rewriteText(responses, current, {
152
+ tldr: tldr === true,
153
+ platform: platform ?? null,
154
+ customPrompt: customPrompt ?? null,
155
+ }, ctx.signal);
156
+ const markdown = tldr ? formatTldrMarkdown(rewritten) : formatMarkdown(rewritten);
157
+ const writeResult = await engine.files.write(scriptAssetId, SCRIPT_FILENAME, markdown);
158
+ if (!writeResult.ok) {
159
+ throw new Error(`Failed to write ${SCRIPT_FILENAME} to ${scriptAssetId}: ${writeResult.error.message}`);
160
+ }
161
+ await engine.metadata.artifacts.write(scriptAssetId, "original", {
162
+ origin: "describe_image",
163
+ sourceAssetId,
164
+ model: INTELLIGENCE_MODEL,
165
+ processed_at: new Date().toISOString(),
166
+ variant: tldr ? "tldr" : "full",
167
+ ...(platform ? { platform } : {}),
168
+ ...(customPrompt ? { customPrompt } : {}),
169
+ });
170
+ return { success: true, scriptAssetId, prompt: rewritten.prompt };
171
+ });
172
+ });
173
+ }