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,18 @@
1
+ import { bookManager } from "../engine.js";
2
+ let pending = null;
3
+ let loadPromise = null;
4
+ export function registerAllJobHandlers(engine, books = bookManager) {
5
+ pending = { engine, books };
6
+ loadPromise = null;
7
+ }
8
+ export function ensureJobHandlersLoaded() {
9
+ if (!pending)
10
+ return Promise.resolve();
11
+ if (!loadPromise) {
12
+ const registration = pending;
13
+ loadPromise = import("./register-all.js").then((mod) => {
14
+ mod.registerAllJobHandlers(registration.engine, registration.books);
15
+ });
16
+ }
17
+ return loadPromise;
18
+ }
@@ -0,0 +1,175 @@
1
+ import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { extname, join } from "node:path";
4
+ import { H264_MEDIUM, probeVideo, runFfmpeg } from "../ffmpeg.js";
5
+ import { queue, registerJobHandler } from "../queue.js";
6
+ import { withLockedLocalLease } from "./_lease.js";
7
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
8
+ import { registerMediaStreams } from "./process-video.js";
9
+ const LOCK_DURATION_MS = 900_000;
10
+ const STALE_VISUAL_DERIVATIVES = [
11
+ ".original.analysis.json",
12
+ ".detect-scenes.json",
13
+ ".suggest-cuts.json",
14
+ ".suggest-sfx.json",
15
+ ".analyze-jokes.json",
16
+ ];
17
+ function outputContainer(path) {
18
+ return extname(path).toLowerCase() === ".mov" ? "mov" : "mp4";
19
+ }
20
+ function buildInjectFirstFrameFilter(width, height) {
21
+ return `[1:v:0]scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},setsar=1[still];[0:v:0][still]overlay=0:0:eof_action=repeat:repeatlast=1:enable='eq(n\\,0)'[video]`;
22
+ }
23
+ async function renderInjectedFirstFrame(inputPath, imagePath, outputPath, width, height, signal) {
24
+ await runFfmpeg([
25
+ "-y",
26
+ "-i", inputPath,
27
+ "-i", imagePath,
28
+ "-filter_complex", buildInjectFirstFrameFilter(width, height),
29
+ "-map", "[video]",
30
+ "-map", "0:a?",
31
+ ...H264_MEDIUM,
32
+ "-pix_fmt", "yuv420p",
33
+ "-c:a", "copy",
34
+ "-map_metadata", "0",
35
+ "-map_chapters", "0",
36
+ "-fps_mode", "passthrough",
37
+ "-movflags", "+faststart",
38
+ "-f", outputContainer(outputPath),
39
+ outputPath,
40
+ ], { signal });
41
+ }
42
+ async function validateInjectedFirstFrame(inputPath, outputPath, signal) {
43
+ const [input, output] = await Promise.all([
44
+ probeVideo(inputPath, { signal }),
45
+ probeVideo(outputPath, { signal }),
46
+ ]);
47
+ if (output.videoCodec === null || output.width <= 0 || output.height <= 0) {
48
+ throw new Error("First-frame output has no usable video stream");
49
+ }
50
+ if (output.width !== input.width || output.height !== input.height) {
51
+ throw new Error(`First-frame output dimensions ${output.width}x${output.height} do not match source ${input.width}x${input.height}`);
52
+ }
53
+ if (output.hasAudio !== input.hasAudio || output.audioCodec !== input.audioCodec) {
54
+ throw new Error("First-frame output audio stream layout or codec does not match the source");
55
+ }
56
+ const tolerance = Math.max(0.08, 2 / input.fps);
57
+ if (Math.abs(output.duration - input.duration) > tolerance) {
58
+ throw new Error(`First-frame output duration ${output.duration.toFixed(3)}s does not match source ${input.duration.toFixed(3)}s`);
59
+ }
60
+ if (Math.abs(output.videoDuration - input.videoDuration) > tolerance) {
61
+ throw new Error("First-frame output video timing does not match the source");
62
+ }
63
+ if (input.audioDuration !== null && output.audioDuration !== null
64
+ && Math.abs(output.audioDuration - input.audioDuration) > tolerance) {
65
+ throw new Error("First-frame output audio timing does not match the source");
66
+ }
67
+ if (Math.abs(output.fps - input.fps) > 0.01) {
68
+ throw new Error(`First-frame output frame rate ${output.fps} does not match source ${input.fps}`);
69
+ }
70
+ }
71
+ async function deleteIfPresent(engine, assetId, filename) {
72
+ const result = await engine.files.delete(assetId, filename);
73
+ if (!result.ok && result.error.code !== "NOT_FOUND") {
74
+ throw new Error(`Failed to delete ${filename}: ${result.error.message}`);
75
+ }
76
+ }
77
+ async function discardTemp(outputPath) {
78
+ await rm(outputPath, { force: true });
79
+ }
80
+ async function replaceAssetFile(engine, assetId, targetFilename, outputPath) {
81
+ await replaceArtifactFileFromPath(engine, assetId, targetFilename, outputPath);
82
+ }
83
+ async function refreshFinalThumbnail(engine, assetId, assetDir, targetFilename, signal) {
84
+ const tempDir = await mkdtemp(join(tmpdir(), "videocity-inject-first-frame-"));
85
+ const thumbnailPath = join(tempDir, "thumbnail.jpg");
86
+ try {
87
+ await runFfmpeg([
88
+ "-y",
89
+ "-i", join(assetDir, targetFilename),
90
+ "-frames:v", "1",
91
+ "-q:v", "2",
92
+ thumbnailPath,
93
+ ], { signal });
94
+ const thumbnailStat = await stat(thumbnailPath).catch(() => null);
95
+ if (!thumbnailStat || thumbnailStat.size === 0) {
96
+ throw new Error("Failed to generate final thumbnail");
97
+ }
98
+ const written = await engine.files.write(assetId, "original.jpg", await readFile(thumbnailPath));
99
+ if (!written.ok)
100
+ throw new Error(`Failed to refresh final thumbnail: ${written.error.message}`);
101
+ }
102
+ finally {
103
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
104
+ }
105
+ }
106
+ export function registerInjectFirstFrameJob(engine) {
107
+ registerJobHandler("inject_first_frame", async (payload, ctx) => {
108
+ const { assetId, assetDir, targetFilename, inputPath, outputPath, imageAssetId, imagePath, bookId, orientation, } = payload;
109
+ const artifact = engine.artifacts.get(assetId);
110
+ const isFinalAsset = artifact.ok && artifact.value.kind === "final";
111
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "process", { durationMs: LOCK_DURATION_MS, data: { state: "injecting-first-frame" } }, async () => {
112
+ const source = await probeVideo(inputPath, { signal: ctx.signal });
113
+ if (source.videoCodec === null || source.width <= 0 || source.height <= 0 || source.fps <= 0) {
114
+ throw new Error(`No usable video stream found in ${targetFilename}`);
115
+ }
116
+ const history = engine.history.artifact(assetId);
117
+ const restoreCommit = history[0]?.hash;
118
+ try {
119
+ await renderInjectedFirstFrame(inputPath, imagePath, outputPath, source.width, source.height, ctx.signal);
120
+ await validateInjectedFirstFrame(inputPath, outputPath, ctx.signal);
121
+ await replaceAssetFile(engine, assetId, targetFilename, outputPath);
122
+ if (isFinalAsset) {
123
+ await registerMediaStreams(engine, assetId, targetFilename, await probeVideo(join(assetDir, targetFilename), { signal: ctx.signal }));
124
+ await refreshFinalThumbnail(engine, assetId, assetDir, targetFilename, ctx.signal);
125
+ }
126
+ else {
127
+ for (const filename of STALE_VISUAL_DERIVATIVES) {
128
+ await deleteIfPresent(engine, assetId, filename);
129
+ }
130
+ }
131
+ await engine.history.recordOperation("inject_first_frame", assetId, {
132
+ image_asset_id: imageAssetId,
133
+ target_filename: targetFilename,
134
+ orientation,
135
+ audio_preserved: true,
136
+ timing_preserved: true,
137
+ });
138
+ }
139
+ catch (error) {
140
+ await discardTemp(outputPath).catch(() => undefined);
141
+ if (restoreCommit)
142
+ await engine.history.restoreArtifact(assetId, restoreCommit);
143
+ throw error;
144
+ }
145
+ return { videoPath: join(assetDir, targetFilename) };
146
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
147
+ if (!isFinalAsset) {
148
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
149
+ type: "process_video",
150
+ videoPath: result.videoPath,
151
+ assetId,
152
+ assetDir,
153
+ bookId,
154
+ provenance: {
155
+ origin: "inject_first_frame",
156
+ image_asset_id: imageAssetId,
157
+ injected_at: new Date().toISOString(),
158
+ },
159
+ force: true,
160
+ skipDownstreamJobs: true,
161
+ preserveAudioArtifacts: true,
162
+ reviveVisualAnalysis: true,
163
+ });
164
+ }
165
+ return {
166
+ success: true,
167
+ target_asset_id: assetId,
168
+ image_asset_id: imageAssetId,
169
+ target_filename: targetFilename,
170
+ orientation,
171
+ audio_preserved: true,
172
+ timing_preserved: true,
173
+ };
174
+ });
175
+ }
@@ -0,0 +1,69 @@
1
+ import { tmpdir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { writeFile, unlink } from "node:fs/promises";
4
+ import { submitKieTask } from "./kie-paygate.js";
5
+ import { uploadBytesToFreeHost, contentHash } from "../upload.js";
6
+ import { registerJobHandler } from "../queue.js";
7
+ import { requireApiKey } from "../api-key.js";
8
+ import { probeVideo, runFfmpeg } from "../ffmpeg.js";
9
+ const MIN_ISOLATION_DURATION = 4.6;
10
+ const PAD_TARGET_DURATION = 5.0;
11
+ export function registerIsolateVocalsJob(engine) {
12
+ registerJobHandler("isolate_vocals", async (payload, ctx) => {
13
+ const { audioFilename, assetId, assetDir, bookId, isAudioAsset, } = payload;
14
+ requireApiKey("KIE_API_KEY");
15
+ // Read audio file from asset
16
+ const readResult = await engine.files.read(assetId, audioFilename);
17
+ if (!readResult.ok)
18
+ throw new Error(readResult.error.message);
19
+ const audioBuffer = Buffer.from(readResult.value);
20
+ // Check duration and pad if needed
21
+ const probe = await probeVideo(join(assetDir, audioFilename));
22
+ let uploadBuffer = audioBuffer;
23
+ let originalDuration;
24
+ if (probe.duration < MIN_ISOLATION_DURATION) {
25
+ originalDuration = probe.duration;
26
+ const padSeconds = PAD_TARGET_DURATION - probe.duration;
27
+ const tmpInput = join(tmpdir(), `isolate-pad-in-${Date.now()}.mp3`);
28
+ const tmpOutput = join(tmpdir(), `isolate-pad-out-${Date.now()}.mp3`);
29
+ try {
30
+ await writeFile(tmpInput, audioBuffer);
31
+ await runFfmpeg([
32
+ "-y", "-i", tmpInput,
33
+ "-af", `apad=pad_dur=${padSeconds}`,
34
+ tmpOutput,
35
+ ]);
36
+ const { readFile: readFs } = await import("node:fs/promises");
37
+ uploadBuffer = await readFs(tmpOutput);
38
+ }
39
+ finally {
40
+ await unlink(tmpInput).catch(() => { });
41
+ await unlink(tmpOutput).catch(() => { });
42
+ }
43
+ }
44
+ // Upload to KIE with a content-hash filename to avoid collisions with
45
+ // other assets that share the same local filename (e.g. `original.mp3`).
46
+ const ext = audioFilename.split(".").pop() ?? "bin";
47
+ const hashedName = `${contentHash(uploadBuffer)}.${ext}`;
48
+ const downloadUrl = await uploadBytesToFreeHost(uploadBuffer, hashedName);
49
+ // Create isolation task
50
+ const request = {
51
+ model: "elevenlabs/audio-isolation",
52
+ input: {
53
+ audio_url: downloadUrl,
54
+ },
55
+ };
56
+ const taskId = await submitKieTask(engine, ctx, {
57
+ request,
58
+ taskType: "isolate_vocals",
59
+ assetId,
60
+ assetDir,
61
+ bookId,
62
+ meta: {
63
+ isAudioAsset,
64
+ ...(originalDuration !== undefined ? { originalDuration } : {}),
65
+ },
66
+ });
67
+ return { success: true, taskId };
68
+ });
69
+ }
@@ -0,0 +1,177 @@
1
+ import { createKie } from "../usage-providers.js";
2
+ import { submitMediaJob } from "@apicity/kie";
3
+ import { mintOtp } from "@apicity/cost";
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { randomBytes } from "node:crypto";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ import { QUEUED_TASK_ID } from "videobook-engine";
9
+ import { requireApiKey } from "../api-key.js";
10
+ import { referenceVideoDuration } from "../reference-video-duration.js";
11
+ import { assertProviderPayload } from "../provider-prompt-transform.js";
12
+ import { assessKieRequestEstimate, logUnavailableCostEstimate, } from "./cost-estimate-assessment.js";
13
+ export const KIE_PAYGATE_DOT_PATH = "api.v1.jobs.createTask";
14
+ function defaultSecretPath() {
15
+ return join(homedir(), ".videobook", "paygate-secret");
16
+ }
17
+ function writeKey(path, content, mode) {
18
+ mkdirSync(dirname(path), { recursive: true });
19
+ writeFileSync(path, content, { encoding: "utf8", mode });
20
+ try {
21
+ chmodSync(path, mode);
22
+ }
23
+ catch {
24
+ return;
25
+ }
26
+ }
27
+ export function ensurePaygateSecret() {
28
+ const configuredSecret = process.env.APICITY_PAYGATE_SECRET?.trim();
29
+ if (configuredSecret)
30
+ return configuredSecret;
31
+ const secretPath = process.env.APICITY_PAYGATE_SECRET_PATH ?? defaultSecretPath();
32
+ if (!existsSync(secretPath)) {
33
+ writeKey(secretPath, `${randomBytes(32).toString("base64url")}\n`, 0o600);
34
+ }
35
+ const secret = readFileSync(secretPath, "utf8").trim();
36
+ if (!secret) {
37
+ throw new Error(`APICITY paygate secret is empty: ${secretPath}`);
38
+ }
39
+ process.env.APICITY_PAYGATE_SECRET_PATH = secretPath;
40
+ return secret;
41
+ }
42
+ export function mintKieOtp(request, ttlSeconds = 10 * 60, dotPath = KIE_PAYGATE_DOT_PATH, _costHints = {}) {
43
+ const payload = request;
44
+ // The OTP hashes the wire payload verbatim — costHints never enter the hash,
45
+ // so the request that hits Kie carries only its documented schema fields.
46
+ // Cost estimation is advisory and must not gate minting.
47
+ return mintOtp(ensurePaygateSecret(), {
48
+ provider: "kie",
49
+ method: "POST",
50
+ dotPath,
51
+ request: payload,
52
+ ttl: ttlSeconds,
53
+ });
54
+ }
55
+ export async function authorizeKiePaygate(request, options = {}) {
56
+ const costHints = options.costHints ?? {};
57
+ const assessment = assessKieRequestEstimate(request, costHints);
58
+ if (!assessment.available) {
59
+ const model = typeof request.model === "string"
60
+ ? request.model
61
+ : options.correlation?.model;
62
+ await logUnavailableCostEstimate({
63
+ assessment,
64
+ engine: options.engine,
65
+ correlation: {
66
+ provider: "kie",
67
+ ...options.correlation,
68
+ ...(model ? { model } : {}),
69
+ },
70
+ });
71
+ }
72
+ return {
73
+ otp: mintKieOtp(request, options.ttlSeconds, options.dotPath, costHints),
74
+ assessment,
75
+ };
76
+ }
77
+ function createKieProvider(apiKey) {
78
+ return createKie({
79
+ apiKey,
80
+ timeout: 60_000,
81
+ paygate: { secret: ensurePaygateSecret() },
82
+ });
83
+ }
84
+ export async function submitKieTask(engine, ctx, args) {
85
+ const request = args.request;
86
+ assertProviderPayload(request, {
87
+ toolName: args.taskType,
88
+ wire: true,
89
+ });
90
+ const apiKey = requireApiKey("KIE_API_KEY");
91
+ await engine.metadata.artifacts.write(args.assetId, "generation-payload", request);
92
+ const costHints = { ...args.costHints };
93
+ const requestInput = request.input;
94
+ const videoUrls = requestInput.reference_video_urls;
95
+ const sourceVideoIds = args.meta.sourceVideoIds;
96
+ if (costHints.inputDuration === undefined && Array.isArray(videoUrls) && videoUrls.length > 0
97
+ && Array.isArray(sourceVideoIds) && sourceVideoIds.length === videoUrls.length
98
+ && sourceVideoIds.every((id) => typeof id === "string")) {
99
+ const duration = await referenceVideoDuration(engine, sourceVideoIds);
100
+ if (duration !== undefined)
101
+ costHints.inputDuration = duration;
102
+ }
103
+ if (Object.keys(costHints).length > 0) {
104
+ await engine.metadata.artifacts.write(args.assetId, "generation-cost-hints", costHints);
105
+ }
106
+ const provider = createKieProvider(apiKey);
107
+ const { otp } = await authorizeKiePaygate(request, {
108
+ costHints,
109
+ engine,
110
+ correlation: {
111
+ bookId: args.bookId,
112
+ artifactId: args.assetId,
113
+ tool: args.taskType,
114
+ stage: "submit-kie",
115
+ },
116
+ });
117
+ const taskId = await submitMediaJob(provider, request, { otp });
118
+ const pendingResult = await engine.jobs.pending.write({
119
+ taskId,
120
+ taskType: args.taskType,
121
+ artifactId: args.assetId,
122
+ meta: args.meta,
123
+ }, ctx.getOwnerId());
124
+ if (!pendingResult.ok)
125
+ throw new Error(`Failed to write pending-task for ${args.assetId}: ${pendingResult.error.message}`);
126
+ if (pendingResult.value === null)
127
+ throw new Error(`Lost local lease while writing pending-task`);
128
+ await engine.metadata.artifacts.write(args.assetId, "kie-task-id", { taskId });
129
+ ctx.setLease({ kind: "task", taskId });
130
+ return taskId;
131
+ }
132
+ export async function submitQueuedKieTask(engine, task) {
133
+ if (task.taskId !== QUEUED_TASK_ID)
134
+ throw new Error(`KIE task ${task.artifactId} is not locally queued`);
135
+ const ownerId = await engine.jobs.pending.getOwner(task.artifactId, QUEUED_TASK_ID);
136
+ if (!ownerId)
137
+ throw new Error(`No active queued KIE lease found for ${task.artifactId}`);
138
+ const payloadResult = await engine.metadata.artifacts.read(task.artifactId, "generation-payload");
139
+ if (!payloadResult.ok || payloadResult.value === null)
140
+ throw new Error(`No prepared KIE payload found for ${task.artifactId}`);
141
+ const request = payloadResult.value;
142
+ assertProviderPayload(request, {
143
+ toolName: task.taskType,
144
+ wire: true,
145
+ });
146
+ // Reload persisted cost hints (if any) so the OTP minted on resubmission
147
+ // sees the same duration the original handler resolved, without leaking it
148
+ // into the wire payload.
149
+ const hintsResult = await engine.metadata.artifacts.read(task.artifactId, "generation-cost-hints");
150
+ const costHints = hintsResult.ok && hintsResult.value ? hintsResult.value : {};
151
+ const apiKey = requireApiKey("KIE_API_KEY");
152
+ const provider = createKieProvider(apiKey);
153
+ const { otp } = await authorizeKiePaygate(request, {
154
+ costHints,
155
+ engine,
156
+ correlation: {
157
+ artifactId: task.artifactId,
158
+ tool: task.taskType,
159
+ stage: "submit-queued-kie",
160
+ },
161
+ });
162
+ const taskId = await submitMediaJob(provider, request, { otp });
163
+ const meta = { ...task.meta };
164
+ delete meta.kiePaygate;
165
+ const writeResult = await engine.jobs.pending.write({
166
+ taskId,
167
+ taskType: task.taskType,
168
+ artifactId: task.artifactId,
169
+ meta,
170
+ }, ownerId);
171
+ if (!writeResult.ok)
172
+ throw new Error(`Failed to write submitted pending-task for ${task.artifactId}: ${writeResult.error.message}`);
173
+ if (writeResult.value === null)
174
+ throw new Error(`Lost queued KIE lease while submitting ${task.artifactId}`);
175
+ await engine.metadata.artifacts.write(task.artifactId, "kie-task-id", { taskId });
176
+ return writeResult.value.task;
177
+ }
@@ -0,0 +1,5 @@
1
+ import { registerJobHandler } from "../queue.js";
2
+ import { labelMusicSections } from "../rhythm/section-labels.js";
3
+ export function registerLabelMusicSectionsJob(engine) {
4
+ registerJobHandler("label_music_sections", async (payload, context) => labelMusicSections(engine, String(payload.artifactId), String(payload.analysisId), String(payload.streamId), context.signal));
5
+ }
@@ -0,0 +1,135 @@
1
+ import { isSubtitleStyle } from "../remotion/subtitles/styles.js";
2
+ import sharp from "sharp";
3
+ import { getApiKey } from "../api-key.js";
4
+ import { sampleEvenly } from "../frame-utils.js";
5
+ import { OSG_PLACEMENT_VERSION, writeOsgPlacementAnalysis, } from "../osg-placement-cache.js";
6
+ import { registerJobHandler } from "../queue.js";
7
+ import { mapSourceRectToTarget, orientationDimensions, subtitleBand, } from "../remotion/keyword-placement.js";
8
+ import { scoreWatermarkCorners } from "../remotion/osg-spec.js";
9
+ import { RENDER_ORIENTATIONS, readStoredRenderDefaults } from "../tools/reconcile-project-work.js";
10
+ import { detectOccupiedRegions } from "../vision-regions.js";
11
+ import { BADGE_ASPECT } from "../watermark-badge.js";
12
+ import { withLockedLocalLease } from "./_lease.js";
13
+ const LOCK_DURATION_MS = 300_000;
14
+ const MAX_FRAMES = 8;
15
+ const SYSTEM_PROMPT = `You are a precise video-layout analyst. Each attached image is a frame sampled evenly across a video clip that will carry a small persistent corner watermark.
16
+
17
+ For every frame, identify all occupied regions the watermark must not cover:
18
+ - every visible human face, including partial faces and faces in posters or screens
19
+ - every existing visible text block, subtitle, caption, title, logo, watermark, or graphic
20
+
21
+ Return one entry for every cue_id. Coordinates use a normalized image scale from 0 to 1000: x and y are the top-left corner, followed by width and height. Use tight boxes around the visible occupied content. Do not return boxes for bodies, clothing, scenery, microphones, or ordinary objects. Return an empty occupied_regions array only when none of the listed content is visible. Never omit, rename, or duplicate a cue_id.`;
22
+ function isSubtitlePosition(value) {
23
+ return value === "top" || value === "middle" || value === "bottom";
24
+ }
25
+ function isFontSizePreset(value) {
26
+ return value === "small" || value === "medium" || value === "large";
27
+ }
28
+ async function readSourceDimensions(engine, assetId, firstFrame) {
29
+ const meta = await engine.metadata.artifacts.read(assetId, "original");
30
+ if (meta.ok
31
+ && typeof meta.value.width === "number" && meta.value.width > 0
32
+ && typeof meta.value.height === "number" && meta.value.height > 0) {
33
+ return { width: meta.value.width, height: meta.value.height };
34
+ }
35
+ // Only the aspect matters for mapping onto the render canvas, so the sampled
36
+ // frame's dimensions are an equivalent fallback.
37
+ const probed = await sharp(firstFrame).metadata();
38
+ if (!probed.width || !probed.height) {
39
+ throw new Error(`Could not determine source dimensions for ${assetId}`);
40
+ }
41
+ return { width: probed.width, height: probed.height };
42
+ }
43
+ async function chooseWatermarkPositions(engine, assetId, regions, sourceWidth, sourceHeight, userScale, badgeAspect) {
44
+ const positions = {};
45
+ for (const orientation of RENDER_ORIENTATIONS) {
46
+ const { width, height } = orientationDimensions(orientation);
47
+ const occupied = regions.map((region) => ({
48
+ ...mapSourceRectToTarget(region, sourceWidth, sourceHeight, width, height),
49
+ kind: region.kind,
50
+ confidence: region.confidence,
51
+ }));
52
+ const stored = await readStoredRenderDefaults(engine, assetId, orientation);
53
+ const band = stored.subtitlesEnabled === true
54
+ ? subtitleBand(orientation, height, isSubtitlePosition(stored.position) ? stored.position : "bottom", isSubtitleStyle(stored.highlightStyle) ? stored.highlightStyle : "impact", isFontSizePreset(stored.fontSize) ? stored.fontSize : "medium")
55
+ : null;
56
+ positions[orientation] = scoreWatermarkCorners(orientation, occupied, {
57
+ aspect: badgeAspect,
58
+ userScale,
59
+ subtitleBand: band,
60
+ }).position;
61
+ }
62
+ return positions;
63
+ }
64
+ /**
65
+ * Sample frames, ask the vision model for occupied regions, and pick the
66
+ * least-busy corner per orientation. Writes the analysis cache on success.
67
+ * Throws when vision is unavailable (no key, no frames) so callers can fall
68
+ * back to a coarser placement.
69
+ */
70
+ export async function analyzeOsgPlacement(engine, request) {
71
+ const { assetId, fingerprint, userScale, badgeAspect = BADGE_ASPECT, signal } = request;
72
+ const apiKey = getApiKey("OPENAI_API_KEY");
73
+ if (!apiKey)
74
+ throw new Error("OPENAI_API_KEY is not set");
75
+ const listResult = await engine.files.listSubdir(assetId, "original_frames");
76
+ const frameFiles = listResult.ok
77
+ ? listResult.value
78
+ .filter((f) => f.endsWith(".jpg"))
79
+ .sort((a, b) => parseFloat(a) - parseFloat(b))
80
+ : [];
81
+ if (frameFiles.length === 0) {
82
+ throw new Error(`no_frames: asset ${assetId} has no original_frames to analyze`);
83
+ }
84
+ const sampled = sampleEvenly(frameFiles, MAX_FRAMES);
85
+ const frames = [];
86
+ for (const file of sampled) {
87
+ signal.throwIfAborted();
88
+ const read = await engine.files.read(assetId, `original_frames/${file}`);
89
+ if (!read.ok)
90
+ throw new Error(read.error.message);
91
+ frames.push({
92
+ id: file,
93
+ label: `timestamp=${parseFloat(file).toFixed(3)}s`,
94
+ buffer: Buffer.from(read.value),
95
+ });
96
+ }
97
+ const source = await readSourceDimensions(engine, assetId, frames[0].buffer);
98
+ const regionsByFrame = await detectOccupiedRegions(frames, {
99
+ apiKey,
100
+ signal,
101
+ instructions: SYSTEM_PROMPT,
102
+ schemaName: "osg_placement_analysis",
103
+ errorLabel: "OSG placement",
104
+ timeout: 240_000,
105
+ });
106
+ const regions = [...regionsByFrame.values()].flat();
107
+ const positions = await chooseWatermarkPositions(engine, assetId, regions, source.width, source.height, userScale, badgeAspect);
108
+ const analysis = {
109
+ version: OSG_PLACEMENT_VERSION,
110
+ fingerprint,
111
+ sourceWidth: source.width,
112
+ sourceHeight: source.height,
113
+ frameTimes: sampled.map((file) => parseFloat(file)),
114
+ regions,
115
+ positions,
116
+ analyzedAt: new Date().toISOString(),
117
+ };
118
+ await writeOsgPlacementAnalysis(engine, assetId, analysis);
119
+ return analysis;
120
+ }
121
+ export function registerOsgPlacementJob(engine) {
122
+ registerJobHandler("osg_placement", async (payload, ctx) => {
123
+ const { assetId, assetDir, fingerprint, userScale, badgeAspect = BADGE_ASPECT, } = payload;
124
+ return withLockedLocalLease(engine, assetId, assetDir, ctx, "analyze", { durationMs: LOCK_DURATION_MS, data: { state: "analyzing-osg-placement" } }, async () => {
125
+ const analysis = await analyzeOsgPlacement(engine, {
126
+ assetId,
127
+ fingerprint,
128
+ userScale,
129
+ badgeAspect,
130
+ signal: ctx.signal,
131
+ });
132
+ return { analysis };
133
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
134
+ });
135
+ }
@@ -0,0 +1,72 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { logger } from "../logger.js";
3
+ import { registerJobHandler, queue } from "../queue.js";
4
+ import { runFfmpeg, probeVideo, H264_MEDIUM, AUDIO_OPTS } from "../ffmpeg.js";
5
+ import { join, basename } from "node:path";
6
+ import { withLockedLocalLease } from "./_lease.js";
7
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
8
+ const LOCK_DURATION_MS = 300_000;
9
+ export function registerPadVideoJob(engine) {
10
+ registerJobHandler("pad_video", async (payload, ctx) => {
11
+ const { sourcePath, assetDir, sourceAssetId, bookId, targetSecs, hasAudio } = payload;
12
+ logger.debug("pad-video-started", { assetId: sourceAssetId, bookId });
13
+ const result = await withLockedLocalLease(engine, sourceAssetId, assetDir, ctx, "process", { durationMs: LOCK_DURATION_MS, data: { state: "padding-video" } }, async () => {
14
+ const outName = `padded-${Date.now()}.mp4`;
15
+ const outputPath = join(assetDir, outName);
16
+ try {
17
+ const source = await probeVideo(sourcePath, { signal: ctx.signal });
18
+ if (!source.videoStream || source.videoDuration <= 0 || targetSecs <= source.videoDuration) {
19
+ throw new Error("Padding must extend a playable video stream");
20
+ }
21
+ const args = ["-y", "-i", sourcePath];
22
+ if (hasAudio) {
23
+ args.push("-filter_complex", `[0:v]tpad=stop=-1:color=black[v];[0:a]atrim=duration=${source.videoDuration},asetpts=PTS-STARTPTS,apad[a]`, "-map", "[v]", "-map", "[a]", "-t", targetSecs.toString(), ...H264_MEDIUM, ...AUDIO_OPTS, outputPath);
24
+ }
25
+ else {
26
+ args.push("-filter_complex", "[0:v]tpad=stop=-1:color=black[v]", "-map", "[v]", "-t", targetSecs.toString(), ...H264_MEDIUM, outputPath);
27
+ }
28
+ await runFfmpeg(args, { signal: ctx.signal });
29
+ const output = await probeVideo(outputPath, { signal: ctx.signal });
30
+ if (!output.videoStream || Math.abs(output.videoDuration - targetSecs) > 1 / source.fps
31
+ || output.hasAudio !== hasAudio) {
32
+ throw new Error("Padding produced an invalid video duration or audio stream");
33
+ }
34
+ const splitAtIso = new Date().toISOString();
35
+ const provenance = {
36
+ origin: "pad_video",
37
+ source_asset_id: sourceAssetId,
38
+ target_secs: targetSecs,
39
+ has_audio: hasAudio,
40
+ split_at_iso: splitAtIso,
41
+ };
42
+ const inputFilename = basename(sourcePath);
43
+ await replaceArtifactFileFromPath(engine, sourceAssetId, inputFilename, outputPath);
44
+ for (const stale of [
45
+ ".original.analysis.json",
46
+ ".analysis.gpt4otrans.json",
47
+ "elevenlabs.json",
48
+ ]) {
49
+ await engine.files.delete(sourceAssetId, stale);
50
+ }
51
+ await engine.metadata.artifacts.write(sourceAssetId, "original", provenance);
52
+ const videoPath = join(assetDir, inputFilename);
53
+ return { videoPath, provenance };
54
+ }
55
+ catch (err) {
56
+ await rm(outputPath, { force: true }).catch(() => { });
57
+ throw err;
58
+ }
59
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
60
+ await queue.enqueueAndWait(`process-video-${sourceAssetId}-${Date.now()}`, {
61
+ type: "process_video",
62
+ videoPath: result.videoPath,
63
+ assetId: sourceAssetId,
64
+ assetDir: assetDir,
65
+ bookId,
66
+ skipDownstreamJobs: true,
67
+ force: true,
68
+ provenance: result.provenance,
69
+ });
70
+ return { success: true };
71
+ });
72
+ }