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,133 @@
1
+ import { execFile } from "node:child_process";
2
+ import { rm } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { basename, extname } from "node:path";
5
+ import { registerJobHandler } from "../queue.js";
6
+ import { FFPROBE, runFfmpeg } from "../ffmpeg.js";
7
+ import { withLockedLocalLease } from "./_lease.js";
8
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
9
+ import { AUDIO_TRIM_EPSILON_SECONDS, validateAudioTrimRange } from "../audio-trim-range.js";
10
+ const execFileAsync = promisify(execFile);
11
+ const LOCK_DURATION_MS = 300_000;
12
+ async function probeAudioDuration(filePath) {
13
+ const { stdout } = await execFileAsync(FFPROBE, [
14
+ "-hide_banner",
15
+ "-v", "error",
16
+ "-print_format", "json",
17
+ "-show_format",
18
+ "-show_streams",
19
+ filePath,
20
+ ]);
21
+ const data = JSON.parse(stdout);
22
+ const audioStream = data.streams?.find((stream) => stream.codec_type === "audio");
23
+ if (!audioStream)
24
+ throw new Error("Audio file has no usable audio stream");
25
+ const formatDuration = Number(data.format?.duration ?? 0);
26
+ const streamDuration = Number(audioStream.duration ?? formatDuration);
27
+ const duration = Number.isFinite(streamDuration) && streamDuration > 0
28
+ ? streamDuration
29
+ : formatDuration;
30
+ if (!Number.isFinite(duration) || duration <= 0) {
31
+ throw new Error(`Audio file has invalid duration (${duration})`);
32
+ }
33
+ return duration;
34
+ }
35
+ function audioCodecArgs(inputPath) {
36
+ const ext = extname(inputPath).toLowerCase();
37
+ if (ext === ".mp3")
38
+ return ["-c:a", "libmp3lame", "-q:a", "2"];
39
+ if (ext === ".ogg")
40
+ return ["-c:a", "libvorbis", "-q:a", "4"];
41
+ if (ext === ".flac")
42
+ return ["-c:a", "flac"];
43
+ if (ext === ".wav")
44
+ return ["-c:a", "pcm_s16le"];
45
+ return ["-c:a", "aac", "-b:a", "192k"];
46
+ }
47
+ function buildKeepArgs(inputPath, outputPath, start, duration) {
48
+ const args = [];
49
+ if (start > 0)
50
+ args.push("-ss", String(start));
51
+ args.push("-i", inputPath, "-map", "0:a:0", "-vn", "-t", String(duration), ...audioCodecArgs(inputPath), "-y", outputPath);
52
+ return args;
53
+ }
54
+ function buildTrimAudioArgs(inputPath, outputPath, sourceDuration, mode, trimStart, trimEnd, cutStart, cutEnd) {
55
+ const expectedDuration = validateAudioTrimRange(sourceDuration, mode, trimStart, trimEnd, cutStart, cutEnd);
56
+ if (mode === "keep") {
57
+ return {
58
+ args: buildKeepArgs(inputPath, outputPath, trimStart ?? 0, expectedDuration),
59
+ expectedDuration,
60
+ };
61
+ }
62
+ const start = cutStart ?? 0;
63
+ const end = Math.min(cutEnd ?? sourceDuration, sourceDuration);
64
+ if (start <= AUDIO_TRIM_EPSILON_SECONDS) {
65
+ return {
66
+ args: buildKeepArgs(inputPath, outputPath, end, expectedDuration),
67
+ expectedDuration,
68
+ };
69
+ }
70
+ if (end >= sourceDuration - AUDIO_TRIM_EPSILON_SECONDS) {
71
+ return {
72
+ args: buildKeepArgs(inputPath, outputPath, 0, expectedDuration),
73
+ expectedDuration,
74
+ };
75
+ }
76
+ const filter = [
77
+ `[0:a]atrim=0:${start},asetpts=PTS-STARTPTS[a1]`,
78
+ `[0:a]atrim=start=${end},asetpts=PTS-STARTPTS[a2]`,
79
+ "[a1][a2]concat=n=2:v=0:a=1[a]",
80
+ ].join(";");
81
+ return {
82
+ args: [
83
+ "-i", inputPath,
84
+ "-filter_complex", filter,
85
+ "-map", "[a]",
86
+ ...audioCodecArgs(inputPath),
87
+ "-y", outputPath,
88
+ ],
89
+ expectedDuration,
90
+ };
91
+ }
92
+ async function runTrimAudio(inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd) {
93
+ const sourceDuration = await probeAudioDuration(inputPath);
94
+ const { args, expectedDuration } = buildTrimAudioArgs(inputPath, outputPath, sourceDuration, mode, trimStart, trimEnd, cutStart, cutEnd);
95
+ await runFfmpeg(args);
96
+ const outputDuration = await probeAudioDuration(outputPath);
97
+ const tolerance = Math.max(0.35, expectedDuration * 0.05);
98
+ if (Math.abs(outputDuration - expectedDuration) > tolerance) {
99
+ throw new Error(`Trimmed audio duration ${outputDuration.toFixed(3)} did not match expected ${expectedDuration.toFixed(3)}`);
100
+ }
101
+ }
102
+ export function registerTrimAudioJob(engine) {
103
+ registerJobHandler("trim_audio", async (payload, ctx) => {
104
+ const { inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd, assetId, assetDir, commitParams, } = payload;
105
+ const inputFilename = basename(inputPath);
106
+ try {
107
+ await withLockedLocalLease(engine, assetId, assetDir, ctx, "trim", { durationMs: LOCK_DURATION_MS, data: { state: "trimming-audio", ...commitParams } }, async () => {
108
+ const original = await engine.files.read(assetId, inputFilename);
109
+ if (!original.ok)
110
+ throw new Error(original.error.message);
111
+ await runTrimAudio(inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd);
112
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
113
+ const provenanceResult = await engine.metadata.artifacts.write(assetId, "original", {
114
+ origin: "trim_audio",
115
+ source_asset_id: assetId,
116
+ ...commitParams,
117
+ trimmed_at: new Date().toISOString(),
118
+ });
119
+ if (!provenanceResult.ok) {
120
+ const rollback = await engine.files.write(assetId, inputFilename, original.value);
121
+ if (!rollback.ok) {
122
+ throw new Error(`${provenanceResult.error.message}; rollback failed: ${rollback.error.message}`);
123
+ }
124
+ throw new Error(provenanceResult.error.message);
125
+ }
126
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
127
+ return { success: true };
128
+ }
129
+ finally {
130
+ await rm(outputPath, { force: true });
131
+ }
132
+ });
133
+ }
@@ -0,0 +1,291 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir, writeFile as fsWriteFile, rename, rm } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { join, basename } from "node:path";
5
+ import { registerJobHandler, queue } from "../queue.js";
6
+ import { runFfmpeg, H264_MEDIUM, AUDIO_OPTS, FFPROBE } from "../ffmpeg.js";
7
+ import { withLockedLocalLease } from "./_lease.js";
8
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
9
+ const execFileAsync = promisify(execFile);
10
+ const LOCK_DURATION_MS = 300_000;
11
+ const CROSSFADE_SECS = 0.03;
12
+ const DURATION_EPSILON_SECS = 0.5;
13
+ // Stream-copy cuts snap to keyframes, so A/V container durations often differ
14
+ // by ~1–2s on long sources. Keep this loose enough to accept those while still
15
+ // rejecting the catastrophic desyncs (e.g. 62s declared with 20s of audio)
16
+ // that prompted this check.
17
+ const AV_DRIFT_TOLERANCE_SECS = 2.0;
18
+ // `ffmpeg -t N` lands on the next frame/sample boundary, so the container's
19
+ // reported format duration is typically 10–50ms longer than N. Pre-shrink by
20
+ // one slack window so the output stays ≤ the requested length — required for
21
+ // downstream caps like Wan 2.7's 10s VideoEdit limit.
22
+ const TRIM_DURATION_SAFETY_SECS = 0.05;
23
+ async function probeStreamDurations(filePath) {
24
+ const { stdout } = await execFileAsync(FFPROBE, [
25
+ "-v", "error",
26
+ "-print_format", "json",
27
+ "-show_streams",
28
+ "-show_format",
29
+ filePath,
30
+ ]);
31
+ const data = JSON.parse(stdout);
32
+ const format = Number(data.format?.duration ?? 0);
33
+ const videoStream = data.streams?.find((s) => s.codec_type === "video");
34
+ const audioStream = data.streams?.find((s) => s.codec_type === "audio");
35
+ const video = videoStream ? Number(videoStream.duration ?? format) : null;
36
+ const audio = audioStream ? Number(audioStream.duration ?? format) : null;
37
+ return { format, video, audio };
38
+ }
39
+ export async function assertSaneTrimOutput(inputPath, outputPath) {
40
+ const [input, output] = await Promise.all([
41
+ probeStreamDurations(inputPath),
42
+ probeStreamDurations(outputPath),
43
+ ]);
44
+ const video = output.video;
45
+ if (!Number.isFinite(output.format) || output.format <= 0
46
+ || video === null || !Number.isFinite(video) || video <= 0) {
47
+ throw new Error(`Trim output has no usable video stream (format=${output.format}, video=${video})`);
48
+ }
49
+ if (output.format > input.format + DURATION_EPSILON_SECS) {
50
+ throw new Error(`Trim output duration ${output.format.toFixed(3)}s exceeds input ${input.format.toFixed(3)}s — likely a corrupted concat`);
51
+ }
52
+ if (output.audio !== null && (!Number.isFinite(output.audio)
53
+ || Math.abs(video - output.audio) > AV_DRIFT_TOLERANCE_SECS)) {
54
+ throw new Error(`Trim output audio/video duration mismatch: video=${video.toFixed(3)}s audio=${output.audio.toFixed(3)}s`);
55
+ }
56
+ }
57
+ // Write to `${outputPath}.tmp` then rename to outputPath, so a partial write
58
+ // (process killed mid-encode, OOM, etc.) never leaves a moov-less file
59
+ // sitting at the canonical asset path. `+faststart` puts moov at the front,
60
+ // which means even a truncated mp4 is partially playable AND surfaces obvious
61
+ // corruption to the user immediately rather than after first probe.
62
+ //
63
+ // Prefer stream-copy: a mid-point split of a multi-minute source must finish in
64
+ // seconds, not re-encode the whole file with libx264 medium (which times out
65
+ // enqueueAndWait and leaves husk assets with original.mp4.tmp). Cuts snap to
66
+ // keyframes. Fall back to re-encode when copy fails or produces a broken file
67
+ // (no mid-GOP keyframes → severe A/V desync).
68
+ const COPY_AV_DRIFT_TOLERANCE_SECS = 2.0;
69
+ const COPY_DURATION_SLACK_SECS = 2.0;
70
+ export async function trimRange(inputPath, outputPath, start, end) {
71
+ try {
72
+ await runTrimRange(inputPath, outputPath, start, end, "copy");
73
+ await assertCopyTrimSane(inputPath, outputPath, start, end);
74
+ }
75
+ catch {
76
+ await rm(outputPath, { force: true });
77
+ await rm(`${outputPath}.tmp`, { force: true });
78
+ await runTrimRange(inputPath, outputPath, start, end, "reencode");
79
+ }
80
+ }
81
+ async function assertCopyTrimSane(inputPath, outputPath, start, end) {
82
+ const [input, output] = await Promise.all([
83
+ probeStreamDurations(inputPath),
84
+ probeStreamDurations(outputPath),
85
+ ]);
86
+ const video = output.video;
87
+ if (!Number.isFinite(output.format) || output.format <= 0
88
+ || video === null || !Number.isFinite(video) || video <= 0) {
89
+ throw new Error(`Copy trim has no usable video stream (format=${output.format}, video=${video})`);
90
+ }
91
+ if (output.audio !== null
92
+ && (!Number.isFinite(output.audio)
93
+ || Math.abs(video - output.audio) > COPY_AV_DRIFT_TOLERANCE_SECS)) {
94
+ throw new Error(`Copy trim A/V desync: video=${video.toFixed(3)}s audio=${output.audio.toFixed(3)}s`);
95
+ }
96
+ let expected;
97
+ if (start !== undefined && end !== undefined)
98
+ expected = end - start;
99
+ else if (end !== undefined)
100
+ expected = end;
101
+ else if (start !== undefined)
102
+ expected = input.format - start;
103
+ if (expected !== undefined && expected > 0) {
104
+ const slack = Math.max(COPY_DURATION_SLACK_SECS, expected * 0.05);
105
+ if (Math.abs(output.format - expected) > slack) {
106
+ throw new Error(`Copy trim duration ${output.format.toFixed(3)}s off expected ${expected.toFixed(3)}s`);
107
+ }
108
+ }
109
+ }
110
+ async function runTrimRange(inputPath, outputPath, start, end, mode) {
111
+ const args = ["-y"];
112
+ // Copy: put -ss AFTER -i so the cut lands on/after the requested time
113
+ // (before -i keyframe-snaps backward and can keep almost the whole source).
114
+ // Re-encode: put -ss BEFORE -i so the decoder skips work.
115
+ if (mode === "reencode" && start !== undefined) {
116
+ args.push("-ss", String(start));
117
+ }
118
+ args.push("-i", inputPath);
119
+ if (mode === "copy" && start !== undefined) {
120
+ args.push("-ss", String(start));
121
+ }
122
+ if (end !== undefined) {
123
+ const requested = start !== undefined ? end - start : end;
124
+ // Safety margin only for re-encode: shaving 50ms on copy can drop a GOP.
125
+ const duration = mode === "reencode"
126
+ ? Math.max(0.01, requested - TRIM_DURATION_SAFETY_SECS)
127
+ : Math.max(0.01, requested);
128
+ args.push("-t", String(duration));
129
+ }
130
+ const tmpPath = `${outputPath}.tmp`;
131
+ if (mode === "copy") {
132
+ args.push("-c", "copy", "-avoid_negative_ts", "make_zero", "-movflags", "+faststart", "-f", "mp4", tmpPath);
133
+ }
134
+ else {
135
+ args.push(...H264_MEDIUM, ...AUDIO_OPTS, "-movflags", "+faststart", "-f", "mp4", tmpPath);
136
+ }
137
+ try {
138
+ await runFfmpeg(args);
139
+ await rename(tmpPath, outputPath);
140
+ }
141
+ catch (err) {
142
+ await rm(tmpPath, { force: true });
143
+ throw err;
144
+ }
145
+ }
146
+ /**
147
+ * Cut exactly `durationSec` seconds starting at `startSec`, with the output's
148
+ * container duration reported as the integer second value (not 9.97 / 10.01).
149
+ *
150
+ * The trick: forcing the output framerate to 24 means a 23.976-fps source
151
+ * (very common — YouTube, most films) becomes 240 frames at 24fps = 10.000s.
152
+ * Audio gets `-t durationSec` directly. With both streams landing on
153
+ * the same integer-second boundary, ffprobe reports `format=duration` as
154
+ * an integer.
155
+ *
156
+ * Used by /even-split where the user wants the first chunk to read as
157
+ * "exactly 10s" / "exactly 9s" — a hard requirement for some video-gen UIs
158
+ * that match on the duration field. No safety margin: precision is the goal.
159
+ */
160
+ const EXACT_COPY_TOLERANCE_SECS = 0.05;
161
+ export async function trimRangeExact(inputPath, outputPath, startSec, durationSec) {
162
+ try {
163
+ await runTrimRange(inputPath, outputPath, startSec > 0 ? startSec : undefined, startSec + durationSec, "copy");
164
+ const copied = await probeStreamDurations(outputPath);
165
+ if (Number.isFinite(copied.format)
166
+ && copied.format > 0
167
+ && copied.format <= durationSec
168
+ && copied.format >= durationSec - EXACT_COPY_TOLERANCE_SECS) {
169
+ await assertSaneTrimOutput(inputPath, outputPath);
170
+ return;
171
+ }
172
+ throw new Error(`Copy trim ${copied.format.toFixed(3)}s is not exact for ${durationSec}s`);
173
+ }
174
+ catch {
175
+ await rm(outputPath, { force: true });
176
+ await rm(`${outputPath}.tmp`, { force: true });
177
+ }
178
+ const args = ["-y"];
179
+ if (startSec > 0)
180
+ args.push("-ss", String(startSec));
181
+ args.push("-i", inputPath);
182
+ args.push("-t", String(durationSec));
183
+ args.push("-r", "24", "-vsync", "cfr");
184
+ const tmpPath = `${outputPath}.tmp`;
185
+ args.push(...H264_MEDIUM, ...AUDIO_OPTS, "-movflags", "+faststart", "-f", "mp4", tmpPath);
186
+ try {
187
+ await runFfmpeg(args);
188
+ await rename(tmpPath, outputPath);
189
+ }
190
+ catch (err) {
191
+ await rm(tmpPath, { force: true });
192
+ throw err;
193
+ }
194
+ }
195
+ async function cutMiddle(inputPath, outputPath, cutStart, cutEnd) {
196
+ // Degenerate cuts: if the cut starts at 0 or ends at/past input duration,
197
+ // one of the two halves would be empty (`-t 0` or seek-past-end) and the
198
+ // subsequent concat fails with "Output file does not contain any stream".
199
+ // Delegate to a single trimRange call in those cases.
200
+ if (cutStart <= 0) {
201
+ await trimRange(inputPath, outputPath, cutEnd, undefined);
202
+ return;
203
+ }
204
+ const { format: inputDuration } = await probeStreamDurations(inputPath);
205
+ if (cutEnd >= inputDuration - DURATION_EPSILON_SECS) {
206
+ await trimRange(inputPath, outputPath, undefined, cutStart);
207
+ return;
208
+ }
209
+ const tempDir = join(outputPath, "..", ".trim_temp");
210
+ await mkdir(tempDir, { recursive: true });
211
+ try {
212
+ const beforePath = join(tempDir, "before.mp4");
213
+ const afterPath = join(tempDir, "after.mp4");
214
+ const concatList = join(tempDir, "concat.txt");
215
+ // The before/after halves read disjoint ranges of the input and write to
216
+ // different output files, so run them in parallel. Halves cut-middle
217
+ // wall-clock on multi-core hosts.
218
+ const fadeOutStart = Math.max(cutStart - CROSSFADE_SECS, 0);
219
+ await Promise.all([
220
+ runFfmpeg([
221
+ "-y", "-i", inputPath,
222
+ "-t", String(cutStart),
223
+ "-af", `afade=t=out:st=${fadeOutStart}:d=${CROSSFADE_SECS}`,
224
+ ...H264_MEDIUM, ...AUDIO_OPTS,
225
+ beforePath,
226
+ ]),
227
+ runFfmpeg([
228
+ "-y", "-ss", String(cutEnd),
229
+ "-i", inputPath,
230
+ "-af", `afade=t=in:d=${CROSSFADE_SECS}`,
231
+ ...H264_MEDIUM, ...AUDIO_OPTS,
232
+ afterPath,
233
+ ]),
234
+ ]);
235
+ await fsWriteFile(concatList, `file '${beforePath}'\nfile '${afterPath}'\n`);
236
+ // Re-encode rather than `-c copy`: the two halves are independently-encoded
237
+ // libx264/AAC streams, so their SPS/PPS, GOP boundaries, and AAC priming
238
+ // samples don't line up — a copy-concat produces a container with bogus
239
+ // duration metadata and a truncated audio stream.
240
+ await runFfmpeg([
241
+ "-y", "-f", "concat", "-safe", "0",
242
+ "-i", concatList,
243
+ ...H264_MEDIUM, ...AUDIO_OPTS,
244
+ outputPath,
245
+ ]);
246
+ }
247
+ finally {
248
+ await rm(tempDir, { recursive: true, force: true });
249
+ }
250
+ }
251
+ export function registerTrimVideoJob(engine) {
252
+ registerJobHandler("trim_video", async (payload, ctx) => {
253
+ const { inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd, assetId, assetDir, bookId, } = payload;
254
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "trim", { durationMs: LOCK_DURATION_MS, data: { state: "trimming" } }, async () => {
255
+ if (mode === "cut") {
256
+ await cutMiddle(inputPath, outputPath, cutStart, cutEnd);
257
+ }
258
+ else {
259
+ await trimRange(inputPath, outputPath, trimStart, trimEnd);
260
+ }
261
+ try {
262
+ await assertSaneTrimOutput(inputPath, outputPath);
263
+ }
264
+ catch (err) {
265
+ await rm(outputPath, { force: true });
266
+ throw err;
267
+ }
268
+ const inputFilename = basename(inputPath);
269
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
270
+ for (const stale of [
271
+ ".original.analysis.json",
272
+ ".analysis.gpt4otrans.json",
273
+ "elevenlabs.json",
274
+ ]) {
275
+ await engine.files.delete(assetId, stale);
276
+ }
277
+ const videoPath = join(assetDir, inputFilename);
278
+ return { videoPath };
279
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
280
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
281
+ type: "process_video",
282
+ videoPath: result.videoPath,
283
+ assetId,
284
+ assetDir,
285
+ bookId,
286
+ provenance: { origin: "trim", trimmed_at: new Date().toISOString() },
287
+ force: true,
288
+ });
289
+ return { success: true };
290
+ });
291
+ }
@@ -0,0 +1,26 @@
1
+ import { createXai } from "../usage-providers.js";
2
+ import { mintOtp } from "@apicity/cost";
3
+ import { ensurePaygateSecret } from "./kie-paygate.js";
4
+ import { assertProviderPayload } from "../provider-prompt-transform.js";
5
+ export const XAI_IMAGE_GENERATIONS_DOT_PATH = "v1.images.generations";
6
+ export const XAI_VIDEO_GENERATIONS_DOT_PATH = "v1.videos.generations";
7
+ export function createXaiPaygatedProvider(apiKey, timeout) {
8
+ return createXai({
9
+ apiKey,
10
+ timeout,
11
+ paygate: { secret: ensurePaygateSecret() },
12
+ });
13
+ }
14
+ export function mintXaiOtp(dotPath, request, ttlSeconds = 10 * 60) {
15
+ return mintOtp(ensurePaygateSecret(), {
16
+ provider: "xai",
17
+ method: "POST",
18
+ dotPath,
19
+ request: request,
20
+ ttl: ttlSeconds,
21
+ });
22
+ }
23
+ export async function callXaiPaid(method, dotPath, request) {
24
+ assertProviderPayload(request, { toolName: dotPath, wire: true });
25
+ return method(request, { otp: mintXaiOtp(dotPath, request) });
26
+ }
@@ -0,0 +1,74 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { KEYWORD_FRAME_CACHE_FILENAME, KEYWORD_PLACEMENT_METADATA_KEY, KEYWORD_PLACEMENT_VERSION, keywordCueId, } from "./remotion/keyword-placement.js";
4
+ const regionSchema = z.object({
5
+ kind: z.enum(["face", "text", "graphic"]),
6
+ confidence: z.number().min(0).max(1),
7
+ x: z.number().min(0).max(1),
8
+ y: z.number().min(0).max(1),
9
+ width: z.number().positive().max(1),
10
+ height: z.number().positive().max(1),
11
+ }).strict().refine((region) => region.x + region.width <= 1.001 && region.y + region.height <= 1.001, { message: "occupied region extends beyond the source frame" });
12
+ const cueSchema = z.object({
13
+ cueId: z.string().min(1),
14
+ text: z.string().min(1),
15
+ frameTime: z.number().nonnegative(),
16
+ regions: z.array(regionSchema),
17
+ }).strict();
18
+ const analysisSchema = z.object({
19
+ version: z.literal(KEYWORD_PLACEMENT_VERSION),
20
+ fingerprint: z.string().min(1),
21
+ sourceWidth: z.number().positive(),
22
+ sourceHeight: z.number().positive(),
23
+ frameCacheFilename: z.literal(KEYWORD_FRAME_CACHE_FILENAME),
24
+ analyzedAt: z.string().min(1),
25
+ cues: z.array(cueSchema).min(1),
26
+ }).strict();
27
+ export function keywordPlacementFingerprint(videoFile, transcript, graphics) {
28
+ const transcriptHash = createHash("sha256").update(transcript).digest("hex");
29
+ const payload = {
30
+ version: KEYWORD_PLACEMENT_VERSION,
31
+ video: {
32
+ name: videoFile.name,
33
+ size: videoFile.sizeBytes,
34
+ objectHash: videoFile.objectHash,
35
+ },
36
+ transcriptHash,
37
+ cues: graphics.map((graphic) => ({
38
+ id: keywordCueId(graphic),
39
+ text: graphic.text,
40
+ start: graphic.start,
41
+ end: graphic.end,
42
+ frameTime: graphic.frameTime,
43
+ })),
44
+ };
45
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
46
+ }
47
+ export function parseKeywordPlacementAnalysis(raw) {
48
+ return analysisSchema.parse(raw);
49
+ }
50
+ export async function readKeywordPlacementAnalysis(engine, assetId) {
51
+ const result = await engine.metadata.artifacts.read(assetId, KEYWORD_PLACEMENT_METADATA_KEY);
52
+ if (!result.ok)
53
+ return null;
54
+ const parsed = analysisSchema.safeParse(result.value);
55
+ return parsed.success ? parsed.data : null;
56
+ }
57
+ export async function writeKeywordPlacementAnalysis(engine, assetId, analysis) {
58
+ const parsed = parseKeywordPlacementAnalysis(analysis);
59
+ const result = await engine.metadata.artifacts.write(assetId, KEYWORD_PLACEMENT_METADATA_KEY, parsed);
60
+ if (!result.ok)
61
+ throw new Error(`Failed to persist keyword placement analysis: ${result.error.message}`);
62
+ }
63
+ export async function isKeywordPlacementCacheValid(engine, assetId, analysis, fingerprint, graphics) {
64
+ if (!analysis || analysis.fingerprint !== fingerprint)
65
+ return false;
66
+ const expectedCueIds = graphics.map(keywordCueId);
67
+ const cachedCueIds = analysis.cues.map((cue) => cue.cueId);
68
+ if (expectedCueIds.length !== cachedCueIds.length)
69
+ return false;
70
+ if (expectedCueIds.some((cueId, index) => cueId !== cachedCueIds[index]))
71
+ return false;
72
+ const frameCache = await engine.files.read(assetId, KEYWORD_FRAME_CACHE_FILENAME);
73
+ return frameCache.ok;
74
+ }
@@ -0,0 +1,142 @@
1
+ import { isSubtitleStyle } from "./remotion/subtitles/styles.js";
2
+ import { isOsgPosition, isOsgSize } from "./osg-assets.js";
3
+ import { queue } from "./queue.js";
4
+ import { readTimeline } from "./timeline-helpers.js";
5
+ import { isLanguageSetting } from "./jobs/_translate-transcript.js";
6
+ import { PLAIN_TRANSCRIPT_FILE, TIMED_TRANSCRIPT_FILE, } from "./transcript-projection.js";
7
+ import { VIDEO_ANALYSIS_FILE } from "./video-analysis-projection.js";
8
+ import { findEligibleRenderActions, readStoredRenderDefaults } from "./tools/reconcile-project-work.js";
9
+ import { DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT, } from "./remotion/keyword-graphics.js";
10
+ function isSubtitlePosition(value) {
11
+ return value === "top" || value === "middle" || value === "bottom";
12
+ }
13
+ function isFontSizePreset(value) {
14
+ return value === "small" || value === "medium" || value === "large";
15
+ }
16
+ function isBackgroundMode(value) {
17
+ return value === "blur" || value === "black";
18
+ }
19
+ function stringOr(value, fallback) {
20
+ return typeof value === "string" ? value : fallback;
21
+ }
22
+ function numberOr(value, fallback) {
23
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
24
+ }
25
+ function nullableString(value) {
26
+ return typeof value === "string" ? value : null;
27
+ }
28
+ function stringArrayOrNull(value) {
29
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
30
+ }
31
+ /**
32
+ * Revive the slot-drop vision analysis for a timeline-assigned video whose
33
+ * .original.analysis.json is missing — the state a force re-process (trim,
34
+ * re-download) leaves behind after it invalidates the analysis sidecars.
35
+ * assign_slot only fires on slot drop, so without this revival the auto
36
+ * pipeline (analyze_video → ElevenLabs transcribe → lazy render) stays wedged
37
+ * and render_timeline reports missing_renders until a manual /render.
38
+ */
39
+ export async function enqueueLazyAnalysisIfSlotted(engine, bookId, assetId) {
40
+ // Same provider gate as assign_slot's lazy analysis: truthy env check (not
41
+ // getApiKey) so placeholder keys used in tests pass; the real placeholder
42
+ // defense lives at the API call inside analyze-video.
43
+ if (!process.env.OPENAI_API_KEY)
44
+ return null;
45
+ const asset = engine.artifacts.get(assetId);
46
+ if (!asset.ok || asset.value.kind !== "video")
47
+ return null;
48
+ const timeline = readTimeline(engine);
49
+ if (!timeline.slots.some((slot) => slot.artifactId === asset.value.artifactId))
50
+ return null;
51
+ const manifestResult = await engine.files.manifest(assetId);
52
+ if (!manifestResult.ok)
53
+ return null;
54
+ const [analysis, plainTranscript, timedTranscript] = await Promise.all([
55
+ engine.files.read(assetId, VIDEO_ANALYSIS_FILE),
56
+ engine.files.read(assetId, PLAIN_TRANSCRIPT_FILE),
57
+ engine.files.read(assetId, TIMED_TRANSCRIPT_FILE),
58
+ ]);
59
+ if (analysis.ok)
60
+ return null;
61
+ // Stable dedupe key: concurrent triggers collapse onto one queued job.
62
+ const jobId = `auto-analyze-${assetId}`;
63
+ await queue.enqueue(jobId, {
64
+ type: "analyze_video",
65
+ assetId,
66
+ assetDir: manifestResult.value.path,
67
+ bookId,
68
+ hasTranscription: plainTranscript.ok || timedTranscript.ok,
69
+ });
70
+ return { jobId };
71
+ }
72
+ async function isRenderAlreadyQueued(engine, assetId, orientation) {
73
+ const work = await engine.jobs.artifactWork.read(assetId);
74
+ if (!work.ok || !work.value)
75
+ return false;
76
+ const state = work.value;
77
+ return state.status !== "ready"
78
+ && state.meta?.kind === "render"
79
+ && state.meta.orientation === orientation;
80
+ }
81
+ export async function enqueueLazyTimelineRender(engine, bookId, assetId, knownAsset) {
82
+ const resolved = engine.artifacts.get(assetId);
83
+ if (!resolved.ok || resolved.value.kind !== "video")
84
+ return null;
85
+ const timeline = readTimeline(engine);
86
+ if (!timeline.slots.some((slot) => slot.artifactId === resolved.value.artifactId))
87
+ return null;
88
+ const orientation = timeline.render;
89
+ if (await isRenderAlreadyQueued(engine, assetId, orientation))
90
+ return null;
91
+ const asset = knownAsset ?? resolved.value;
92
+ const renderActions = await findEligibleRenderActions(engine, [asset], orientation);
93
+ if (renderActions.length === 0)
94
+ return null;
95
+ const action = renderActions[0];
96
+ const stored = await readStoredRenderDefaults(engine, assetId, orientation);
97
+ const subtitlesEnabled = action.hasElevenlabs && stored.subtitlesEnabled === true;
98
+ // Stable dedupe key: concurrent triggers (job-completion hooks racing a
99
+ // sync poll) collapse onto one queued render instead of double-rendering.
100
+ const jobId = `auto-render-${assetId}-${orientation}`;
101
+ await queue.enqueue(jobId, {
102
+ type: "render_project",
103
+ bookId,
104
+ assetId: action.assetId,
105
+ orientation,
106
+ assets: [{
107
+ assetId: action.assetId,
108
+ assetDir: action.assetDir,
109
+ videoPath: action.videoPath,
110
+ videoFilename: action.videoFilename,
111
+ }],
112
+ subtitlesEnabled,
113
+ subtitleLanguage: isLanguageSetting(stored.subtitleLanguage) ? stored.subtitleLanguage : "original",
114
+ highlightStyle: isSubtitleStyle(stored.highlightStyle) ? stored.highlightStyle : "highlight",
115
+ position: isSubtitlePosition(stored.position) ? stored.position : "bottom",
116
+ fontSize: isFontSizePreset(stored.fontSize) ? stored.fontSize : "medium",
117
+ textColor: stringOr(stored.textColor, "#FFFFFF"),
118
+ highlightColor: stringOr(stored.highlightColor, "#991B1B"),
119
+ audioFadeIn: numberOr(stored.audioFadeIn, 0),
120
+ audioFadeOut: numberOr(stored.audioFadeOut, 0),
121
+ audioVolume: numberOr(stored.audioVolume, 100),
122
+ osgImagePath: nullableString(stored.osgImagePath),
123
+ osgAssetId: nullableString(stored.osgAssetId),
124
+ osgPosition: isOsgPosition(stored.osgPosition) ? stored.osgPosition : "top-right",
125
+ osgSize: isOsgSize(stored.osgSize) ? stored.osgSize : "medium",
126
+ osgScale: numberOr(stored.osgScale, 1),
127
+ osgOpacity: numberOr(stored.osgOpacity, 1),
128
+ osgStartSeconds: typeof stored.osgStartSeconds === "number" ? stored.osgStartSeconds : null,
129
+ osgEndSeconds: typeof stored.osgEndSeconds === "number" ? stored.osgEndSeconds : null,
130
+ osgTailSeconds: typeof stored.osgTailSeconds === "number" ? stored.osgTailSeconds : null,
131
+ keywordGraphicsEnabled: stored.keywordGraphicsEnabled === true,
132
+ keywordGraphicsMaxCount: numberOr(stored.keywordGraphicsMaxCount, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT),
133
+ keywordGraphicsLingerSeconds: numberOr(stored.keywordGraphicsLingerSeconds, DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS),
134
+ keywordGraphicsKeywords: stringArrayOrNull(stored.keywordGraphicsKeywords),
135
+ backgroundMode: isBackgroundMode(stored.backgroundMode) ? stored.backgroundMode : "blur",
136
+ strokeWidth: numberOr(stored.strokeWidth, 6),
137
+ strokeColor: stringOr(stored.strokeColor, "#000000"),
138
+ shadowBlur: numberOr(stored.shadowBlur, 6),
139
+ shadowOpacity: numberOr(stored.shadowOpacity, 60),
140
+ });
141
+ return { jobId };
142
+ }