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,316 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { AUDIO_OPTS, H264_MEDIUM, probeVideo, runFfmpeg } from "../ffmpeg.js";
4
+ import { logger } from "../logger.js";
5
+ import { queue, registerJobHandler } from "../queue.js";
6
+ import { scoreWatermarkCornersForCanvas, WATERMARK_MIN_DIM_FRACTION, WATERMARK_OPACITY, } from "../remotion/osg-spec.js";
7
+ import { BADGE_WIDTH, DEFAULT_HANDLE_PADDING, HANDLE_ONLY_BADGE_HEIGHT, handlePillHeight, } from "../watermark-badge.js";
8
+ import { DEFAULT_HANDLE_VERTICAL_PERCENT, watermarkHandleOverlayPosition, } from "../watermark-handle-position.js";
9
+ import { analyzeOsgPlacement } from "./osg-placement.js";
10
+ import { replaceArtifactFileFromPath } from "./_persist-output.js";
11
+ import { withLockedLocalLease } from "./_lease.js";
12
+ import { effectiveWatermarkTiming } from "../watermark-timing.js";
13
+ const LOCK_DURATION_MS = 600_000;
14
+ /** Fade applied at a watermark edge that falls inside the clip. */
15
+ const DEFAULT_WATERMARK_FADE_SECONDS = 0.5;
16
+ export const MAX_WATERMARK_FADE_SECONDS = 2;
17
+ /** Extra looped badge frames past the last fade so overlay never runs dry mid-ramp. */
18
+ const LOOP_MARGIN_SECONDS = 0.25;
19
+ /** Ends this close to the clip end count as "whole clip": no fade out. */
20
+ const CLIP_END_TOLERANCE_SECONDS = 0.001;
21
+ const NO_FADES = { fadeIn: 0, fadeOut: 0 };
22
+ function roundSeconds(value) {
23
+ return Math.round(value * 1000) / 1000;
24
+ }
25
+ /** ffmpeg overlay x/y expressions for a corner, inset by `padding` pixels. */
26
+ function burnOverlayPosition(corner, padding) {
27
+ const [vertical, horizontal] = corner.split("-");
28
+ const x = horizontal === "left"
29
+ ? String(padding)
30
+ : horizontal === "right"
31
+ ? `main_w-overlay_w-${padding}`
32
+ : "(main_w-overlay_w)/2";
33
+ const y = vertical === "top"
34
+ ? String(padding)
35
+ : vertical === "bottom"
36
+ ? `main_h-overlay_h-${padding}`
37
+ : "(main_h-overlay_h)/2";
38
+ return { x, y };
39
+ }
40
+ export function burnWatermarkEnable(startSeconds, endSeconds) {
41
+ if (startSeconds === undefined && endSeconds === undefined)
42
+ return "";
43
+ const start = startSeconds ?? 0;
44
+ return endSeconds === undefined
45
+ ? `:enable='gte(t,${start})'`
46
+ : `:enable='between(t,${start},${endSeconds})'`;
47
+ }
48
+ /**
49
+ * Fade lengths for one watermark window. A watermark fades in only when it
50
+ * starts after the clip start and out only when it ends before the clip end,
51
+ * so a whole-clip watermark keeps hard edges. Fades shrink to half the window
52
+ * so a short window still reaches full opacity in the middle.
53
+ */
54
+ export function burnWatermarkFades(startSeconds, endSeconds, durationSeconds, fadeSeconds) {
55
+ const start = Math.max(0, startSeconds ?? 0);
56
+ const clipEnd = endSeconds === undefined
57
+ ? durationSeconds
58
+ : Math.min(endSeconds, durationSeconds);
59
+ const window = clipEnd - start;
60
+ if (!(fadeSeconds > 0) || !(window > 0))
61
+ return NO_FADES;
62
+ const fade = roundSeconds(Math.min(fadeSeconds, window / 2));
63
+ const endsEarly = endSeconds !== undefined
64
+ && endSeconds < durationSeconds - CLIP_END_TOLERANCE_SECONDS;
65
+ return {
66
+ fadeIn: start > 0 ? fade : 0,
67
+ fadeOut: endsEarly ? fade : 0,
68
+ };
69
+ }
70
+ /**
71
+ * Alpha fades appended to a badge input chain. The badge input must be a
72
+ * timed stream (see `burnWatermarkInputArgs`) for the fade timestamps to
73
+ * line up with the main video.
74
+ */
75
+ function burnWatermarkFadeFilters(fades, startSeconds, endSeconds) {
76
+ const parts = [];
77
+ if (fades.fadeIn > 0) {
78
+ parts.push(`fade=t=in:st=${startSeconds ?? 0}:d=${fades.fadeIn}:alpha=1`);
79
+ }
80
+ if (fades.fadeOut > 0 && endSeconds !== undefined) {
81
+ parts.push(`fade=t=out:st=${roundSeconds(endSeconds - fades.fadeOut)}:d=${fades.fadeOut}:alpha=1`);
82
+ }
83
+ return parts.map((part) => `,${part}`).join("");
84
+ }
85
+ /**
86
+ * How long the looped badge stream must run for its fades to complete: past
87
+ * the last ramp the badge is static, and overlay repeats the final frame for
88
+ * the rest of the clip. Null when the badge needs no fades and a single still
89
+ * frame does.
90
+ */
91
+ export function burnWatermarkLoopSeconds(fades, startSeconds, endSeconds, durationSeconds) {
92
+ if (fades.fadeIn === 0 && fades.fadeOut === 0)
93
+ return null;
94
+ const settledAt = fades.fadeOut > 0 && endSeconds !== undefined
95
+ ? endSeconds
96
+ : (startSeconds ?? 0) + fades.fadeIn;
97
+ return roundSeconds(Math.min(durationSeconds, settledAt + LOOP_MARGIN_SECONDS));
98
+ }
99
+ /** Input args for one badge image: a still frame, or a timed loop when it fades. */
100
+ export function burnWatermarkInputArgs(path, loopSeconds, frameRate) {
101
+ return loopSeconds === null
102
+ ? ["-i", path]
103
+ : ["-loop", "1", "-framerate", frameRate, "-t", String(loopSeconds), "-i", path];
104
+ }
105
+ function burnWatermarkFrameRate(probe) {
106
+ const nominal = probe.videoStream?.video?.nominalFrameRate;
107
+ if (nominal && nominal.numerator > 0 && nominal.denominator > 0) {
108
+ return `${nominal.numerator}/${nominal.denominator}`;
109
+ }
110
+ return String(roundSeconds(probe.fps > 0 ? probe.fps : 30));
111
+ }
112
+ export function buildBurnWatermarkFilter(options) {
113
+ const minDim = Math.min(options.width, options.height);
114
+ const badgeHeight = Math.max(2, 2 * Math.round((minDim * WATERMARK_MIN_DIM_FRACTION * options.userScale) / 2));
115
+ const padding = Math.round(options.width * 0.02);
116
+ const { x, y } = burnOverlayPosition(options.corner, padding);
117
+ const handlePlacement = options.includeQr && options.handlePlacement
118
+ ? options.handlePlacement
119
+ : "center";
120
+ const attachedToQr = handlePlacement !== "center";
121
+ const handleBadgeHeight = handlePillHeight(options.handlePaddingY ?? DEFAULT_HANDLE_PADDING);
122
+ const independentHandleHeight = Math.max(2, 2 * Math.round((badgeHeight * handleBadgeHeight / HANDLE_ONLY_BADGE_HEIGHT) / 2));
123
+ const attachedHandleHeight = Math.max(2, Math.round(badgeHeight * handleBadgeHeight / BADGE_WIDTH));
124
+ const handleScale = attachedToQr
125
+ ? `${badgeHeight}:-2`
126
+ : `-2:${independentHandleHeight}`;
127
+ const handleOverlay = watermarkHandleOverlayPosition({
128
+ placement: handlePlacement,
129
+ corner: options.corner,
130
+ canvasWidth: options.width,
131
+ canvasHeight: options.height,
132
+ qrSize: badgeHeight,
133
+ handleWidth: attachedToQr
134
+ ? badgeHeight
135
+ : Math.round(independentHandleHeight * BADGE_WIDTH / handleBadgeHeight),
136
+ handleHeight: attachedToQr ? attachedHandleHeight : independentHandleHeight,
137
+ padding,
138
+ handleVerticalPercent: options.handleVerticalPercent ?? DEFAULT_HANDLE_VERTICAL_PERCENT,
139
+ });
140
+ const sharedOpacity = options.opacity ?? WATERMARK_OPACITY;
141
+ const qrOpacity = options.qrOpacity ?? sharedOpacity;
142
+ const handleOpacity = options.handleOpacity ?? sharedOpacity;
143
+ const qrStart = options.qrStartSeconds ?? options.startSeconds;
144
+ const qrEnd = options.qrEndSeconds ?? options.endSeconds;
145
+ const handleStart = options.handleStartSeconds ?? options.startSeconds;
146
+ const handleEnd = options.handleEndSeconds ?? options.endSeconds;
147
+ const fadesFor = (start, end) => options.durationSeconds !== undefined && options.fadeSeconds !== undefined
148
+ ? burnWatermarkFades(start, end, options.durationSeconds, options.fadeSeconds)
149
+ : NO_FADES;
150
+ const qrFade = burnWatermarkFadeFilters(fadesFor(qrStart, qrEnd), qrStart, qrEnd);
151
+ const handleFade = burnWatermarkFadeFilters(fadesFor(handleStart, handleEnd), handleStart, handleEnd);
152
+ const qrEnable = burnWatermarkEnable(qrStart, qrEnd);
153
+ const handleEnable = burnWatermarkEnable(handleStart, handleEnd);
154
+ const handleInput = options.includeQr ? 2 : 1;
155
+ if (!options.includeQr && !options.includeHandle) {
156
+ throw new Error("QR or handle watermark input is required");
157
+ }
158
+ const filters = [];
159
+ if (options.includeQr) {
160
+ filters.push(`[1:v]scale=-2:${badgeHeight},format=rgba,colorchannelmixer=aa=${qrOpacity}${qrFade}[qr]`);
161
+ }
162
+ if (options.includeHandle) {
163
+ filters.push(`[${handleInput}:v]scale=${handleScale},format=rgba,colorchannelmixer=aa=${handleOpacity}${handleFade}[handle]`);
164
+ }
165
+ if (options.includeQr) {
166
+ const output = options.includeHandle ? "withqr" : "vout";
167
+ filters.push(`[0:v][qr]overlay=${x}:${y}:format=auto${qrEnable}[${output}]`);
168
+ }
169
+ if (options.includeHandle) {
170
+ const base = options.includeQr ? "withqr" : "0:v";
171
+ filters.push(`[${base}][handle]overlay=${handleOverlay.x}:${handleOverlay.y}:format=auto${handleEnable}[vout]`);
172
+ }
173
+ return filters.join(";");
174
+ }
175
+ function badgeInputArgs(path, timing, probe, fadeSeconds) {
176
+ const fades = burnWatermarkFades(timing.startSeconds, timing.endSeconds, probe.videoDuration, fadeSeconds);
177
+ const loopSeconds = burnWatermarkLoopSeconds(fades, timing.startSeconds, timing.endSeconds, probe.videoDuration);
178
+ return burnWatermarkInputArgs(path, loopSeconds, burnWatermarkFrameRate(probe));
179
+ }
180
+ /**
181
+ * Resolve the QR corner inside the job: a deferred vision pass when the tool
182
+ * asked for one, otherwise the corner the tool already settled on. Vision
183
+ * failures degrade to the tool's fallback rather than failing the burn.
184
+ */
185
+ async function resolveBurnCorner(engine, payload, probe, signal) {
186
+ const fallback = {
187
+ corner: payload.corner,
188
+ placementSource: payload.placementSource ?? "explicit",
189
+ };
190
+ if (!payload.placement)
191
+ return fallback;
192
+ try {
193
+ const analysis = await analyzeOsgPlacement(engine, {
194
+ assetId: payload.assetId,
195
+ fingerprint: payload.placement.fingerprint,
196
+ userScale: payload.userScale,
197
+ badgeAspect: payload.placement.badgeAspect,
198
+ signal,
199
+ });
200
+ // Score corners against the source frame itself — a burned watermark
201
+ // lives in source pixels, so one corner serves every orientation.
202
+ const corner = scoreWatermarkCornersForCanvas(probe.width, probe.height, analysis.regions, {
203
+ aspect: payload.placement.badgeAspect,
204
+ userScale: payload.userScale,
205
+ }).position;
206
+ return { corner, placementSource: "vision" };
207
+ }
208
+ catch (err) {
209
+ signal.throwIfAborted();
210
+ logger.warn("watermark-placement-fallback", { assetId: payload.assetId, error: err });
211
+ return fallback;
212
+ }
213
+ }
214
+ /**
215
+ * Composite the watermark badge onto the source video: scaled to the min-dim
216
+ * fraction the render-time overlay uses, faded to `opacity`, anchored at
217
+ * `corner` with the standard edge padding, and faded in/out at any edge that
218
+ * falls inside the clip. Mirrors the trim/crop shape: replace the canonical
219
+ * file in place, drop stale visual analysis, reprocess.
220
+ */
221
+ export function registerBurnWatermarkJob(engine) {
222
+ registerJobHandler("burn_watermark", async (rawPayload, ctx) => {
223
+ const payload = rawPayload;
224
+ const { assetId, assetDir, bookId, videoPath, qrPath, handlePath, opacity, userScale, qrOpacity, handleOpacity, handleVerticalPercent = DEFAULT_HANDLE_VERTICAL_PERCENT, handlePlacement = "center", handlePaddingY = DEFAULT_HANDLE_PADDING, fadeSeconds = DEFAULT_WATERMARK_FADE_SECONDS, startSeconds, endSeconds, qrStartSeconds, qrEndSeconds, handleStartSeconds, handleEndSeconds, } = payload;
225
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "process", { durationMs: LOCK_DURATION_MS, data: { state: "burning-watermark" } }, async () => {
226
+ const probe = await probeVideo(videoPath, { signal: ctx.signal });
227
+ const handleTiming = handlePath
228
+ ? effectiveWatermarkTiming("Handle watermark", probe.videoDuration, handleStartSeconds ?? startSeconds, handleEndSeconds ?? endSeconds)
229
+ : {};
230
+ const qrTiming = qrPath
231
+ ? effectiveWatermarkTiming("QR watermark", probe.videoDuration, qrStartSeconds ?? startSeconds, qrEndSeconds ?? endSeconds)
232
+ : {};
233
+ const placement = await resolveBurnCorner(engine, payload, probe, ctx.signal);
234
+ const outputPath = join(assetDir, "original_watermarked.mp4");
235
+ const filter = buildBurnWatermarkFilter({
236
+ width: probe.width,
237
+ height: probe.height,
238
+ corner: placement.corner,
239
+ opacity,
240
+ qrOpacity,
241
+ handleOpacity,
242
+ userScale,
243
+ includeQr: qrPath !== undefined,
244
+ includeHandle: handlePath !== undefined,
245
+ handleVerticalPercent,
246
+ handlePlacement,
247
+ handlePaddingY,
248
+ durationSeconds: probe.videoDuration,
249
+ fadeSeconds,
250
+ ...(qrTiming.startSeconds !== undefined
251
+ ? { qrStartSeconds: qrTiming.startSeconds }
252
+ : {}),
253
+ ...(qrTiming.endSeconds !== undefined
254
+ ? { qrEndSeconds: qrTiming.endSeconds }
255
+ : {}),
256
+ ...(handleTiming.startSeconds !== undefined
257
+ ? { handleStartSeconds: handleTiming.startSeconds }
258
+ : {}),
259
+ ...(handleTiming.endSeconds !== undefined
260
+ ? { handleEndSeconds: handleTiming.endSeconds }
261
+ : {}),
262
+ });
263
+ const watermarkInputs = [
264
+ ...(qrPath ? badgeInputArgs(qrPath, qrTiming, probe, fadeSeconds) : []),
265
+ ...(handlePath ? badgeInputArgs(handlePath, handleTiming, probe, fadeSeconds) : []),
266
+ ];
267
+ try {
268
+ await runFfmpeg([
269
+ "-y",
270
+ "-i", videoPath,
271
+ ...watermarkInputs,
272
+ "-filter_complex", filter,
273
+ "-map", "[vout]",
274
+ "-map", "0:a?",
275
+ ...H264_MEDIUM,
276
+ "-pix_fmt", "yuv420p",
277
+ ...AUDIO_OPTS,
278
+ outputPath,
279
+ ], { signal: ctx.signal });
280
+ const check = await probeVideo(outputPath, { signal: ctx.signal });
281
+ if (check.width !== probe.width || check.height !== probe.height
282
+ || Math.abs(check.duration - probe.duration) > 0.5) {
283
+ throw new Error(`Watermarked output looks wrong: ${check.width}x${check.height} ${check.duration.toFixed(2)}s`
284
+ + ` from ${probe.width}x${probe.height} ${probe.duration.toFixed(2)}s`);
285
+ }
286
+ }
287
+ catch (err) {
288
+ await rm(outputPath, { force: true });
289
+ throw err;
290
+ }
291
+ const inputFilename = basename(videoPath);
292
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
293
+ // Visual analysis now describes the un-watermarked frames; transcripts
294
+ // stay valid (audio and timing are untouched).
295
+ await engine.files.delete(assetId, ".original.analysis.json");
296
+ return { videoPath: join(assetDir, inputFilename), placement };
297
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
298
+ // The badge now lives in the pixels. The forced reprocess drops every
299
+ // stored render-time overlay setting, so a later render can't stamp it
300
+ // twice.
301
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
302
+ type: "process_video",
303
+ videoPath: result.videoPath,
304
+ assetId,
305
+ assetDir,
306
+ bookId,
307
+ provenance: { origin: "watermark", watermarked_at: new Date().toISOString() },
308
+ force: true,
309
+ });
310
+ return {
311
+ success: true,
312
+ corner: result.placement.corner,
313
+ placementSource: result.placement.placementSource,
314
+ };
315
+ });
316
+ }
@@ -0,0 +1,176 @@
1
+ import { execFile } from "node:child_process";
2
+ import { rm } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import { basename, join } from "node:path";
5
+ import { registerJobHandler, queue } from "../queue.js";
6
+ import { getFFmpeggy, probeVideo, FFPROBE, H264_MEDIUM, AUDIO_OPTS, } 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 ATEMPO_MIN = 0.5;
12
+ // The muxer can pad the final frame's display duration past the ideal end
13
+ // (observed: 1.5 source frame intervals on B-frame sources); keep the
14
+ // duration check as a backstop and let frame-count equality carry precision.
15
+ const DURATION_FRAME_TOLERANCE = 1.5;
16
+ const DURATION_SLACK_S = 0.03;
17
+ const AUDIO_TOLERANCE_S = 0.15;
18
+ function buildAtempoChain(speed) {
19
+ const filters = [];
20
+ let remaining = speed;
21
+ while (remaining < ATEMPO_MIN) {
22
+ filters.push(`atempo=${ATEMPO_MIN}`);
23
+ remaining /= ATEMPO_MIN;
24
+ }
25
+ filters.push(`atempo=${remaining}`);
26
+ return filters;
27
+ }
28
+ function buildSpeedOutputOptions(speed, hasAudio, targetDuration, outputFrameRate) {
29
+ const videoChain = [`settb=AVTB`, `setpts=(PTS-STARTPTS)/${speed}`];
30
+ const audioChain = ["asetpts=PTS-STARTPTS", ...buildAtempoChain(speed)];
31
+ if (targetDuration !== undefined) {
32
+ const cap = targetDuration.toFixed(6);
33
+ videoChain.push(`trim=end=${cap}`);
34
+ audioChain.push(`atrim=end=${cap}`);
35
+ }
36
+ const opts = [
37
+ "-fps_mode:v", outputFrameRate === undefined ? "passthrough" : "cfr",
38
+ ...(outputFrameRate === undefined ? [] : ["-r", String(outputFrameRate)]),
39
+ "-filter:v", videoChain.join(","),
40
+ ];
41
+ if (hasAudio) {
42
+ opts.push("-filter:a", audioChain.join(","));
43
+ }
44
+ else {
45
+ opts.push("-an");
46
+ }
47
+ opts.push(...H264_MEDIUM, ...AUDIO_OPTS);
48
+ return opts;
49
+ }
50
+ async function probeVideoFrameCount(filePath) {
51
+ try {
52
+ const { stdout } = await execFileAsync(FFPROBE, [
53
+ "-v", "error",
54
+ "-select_streams", "v:0",
55
+ "-show_entries", "stream=nb_frames",
56
+ "-of", "json",
57
+ filePath,
58
+ ]);
59
+ const data = JSON.parse(stdout);
60
+ const count = Number(data.streams?.[0]?.nb_frames);
61
+ return Number.isSafeInteger(count) && count > 0 ? count : null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ async function hasConstantFrameSpacing(filePath) {
68
+ try {
69
+ const { stdout } = await execFileAsync(FFPROBE, [
70
+ "-v", "error",
71
+ "-select_streams", "v:0",
72
+ "-show_entries", "packet=pts",
73
+ "-of", "json",
74
+ filePath,
75
+ ], { maxBuffer: 16 * 1024 * 1024 });
76
+ const data = JSON.parse(stdout);
77
+ const times = data.packets?.map((packet) => Number(packet.pts)) ?? [];
78
+ if (times.length < 2 || times.some((time) => !Number.isSafeInteger(time)))
79
+ return false;
80
+ times.sort((a, b) => a - b);
81
+ const interval = (times[times.length - 1] - times[0]) / (times.length - 1);
82
+ return interval > 0 && times.slice(1).every((time, index) => (time > times[index] && Math.abs(time - times[index] - interval) <= 1));
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function retimeValidationErrors(input, output, speed, targetDuration) {
89
+ const errors = [];
90
+ if (output.videoCodec === null || output.videoDuration <= 0) {
91
+ errors.push("output has no playable video stream");
92
+ return errors;
93
+ }
94
+ // In exact-duration mode the trim/atrim cap wins when a stream would
95
+ // overrun the target, so expectations clamp to the cap.
96
+ const cap = targetDuration ?? Number.POSITIVE_INFINITY;
97
+ const frameInterval = input.fps > 0 ? 1 / input.fps : 1 / 30;
98
+ const videoTolerance = DURATION_FRAME_TOLERANCE * frameInterval / speed + DURATION_SLACK_S;
99
+ const expectedVideo = Math.min(input.videoDuration / speed, cap);
100
+ if (Math.abs(output.videoDuration - expectedVideo) > videoTolerance) {
101
+ errors.push(`video duration ${output.videoDuration.toFixed(3)}s does not match expected`
102
+ + ` ${expectedVideo.toFixed(3)}s (source ${input.videoDuration.toFixed(3)}s at ${speed}x)`);
103
+ }
104
+ if (input.hasAudio) {
105
+ if (!output.hasAudio || output.audioDuration === null) {
106
+ errors.push("output lost the source audio track");
107
+ }
108
+ else {
109
+ const expectedAudio = Math.min((input.audioDuration ?? input.videoDuration) / speed, cap);
110
+ if (Math.abs(output.audioDuration - expectedAudio) > AUDIO_TOLERANCE_S) {
111
+ errors.push(`audio duration ${output.audioDuration.toFixed(3)}s does not match expected`
112
+ + ` ${expectedAudio.toFixed(3)}s (source audio retimed at ${speed}x)`);
113
+ }
114
+ }
115
+ }
116
+ return errors;
117
+ }
118
+ export function registerChangeVideoSpeedJob(engine) {
119
+ registerJobHandler("change_video_speed", async (payload, ctx) => {
120
+ const { inputPath, outputPath, speed, targetDuration, hasAudio, assetId, assetDir, bookId, } = payload;
121
+ const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "change_speed", { durationMs: LOCK_DURATION_MS, data: { state: "changing-speed" } }, async () => {
122
+ const inputProbe = await probeVideo(inputPath);
123
+ const sourceFrameCount = await probeVideoFrameCount(inputPath);
124
+ const outputFrameRate = sourceFrameCount !== null && inputProbe.videoDuration > 0
125
+ && await hasConstantFrameSpacing(inputPath)
126
+ ? sourceFrameCount / (targetDuration ?? inputProbe.videoDuration / speed)
127
+ : undefined;
128
+ const outputOptions = buildSpeedOutputOptions(speed, hasAudio, targetDuration, outputFrameRate);
129
+ const FFmpeggy = await getFFmpeggy();
130
+ const ff = new FFmpeggy({
131
+ input: inputPath,
132
+ output: outputPath,
133
+ outputOptions,
134
+ overwriteExisting: true,
135
+ });
136
+ await ff.run();
137
+ await ff.done();
138
+ // Validate before touching canonical media: a retime that dropped
139
+ // frames, drifted from the expected duration, or lost the audio track
140
+ // must fail the job and leave the current version recoverable.
141
+ const outputProbe = await probeVideo(outputPath);
142
+ const errors = retimeValidationErrors(inputProbe, outputProbe, speed, targetDuration);
143
+ const outputFrameCount = await probeVideoFrameCount(outputPath);
144
+ if (sourceFrameCount !== null
145
+ && outputFrameCount !== null
146
+ && sourceFrameCount !== outputFrameCount) {
147
+ errors.push(`video frame count ${outputFrameCount} does not match source ${sourceFrameCount}`);
148
+ }
149
+ if (errors.length > 0) {
150
+ await rm(outputPath, { force: true });
151
+ throw new Error(`change_video_speed produced invalid output for ${assetId}: ${errors.join("; ")}`);
152
+ }
153
+ const inputFilename = basename(inputPath);
154
+ await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
155
+ const videoPath = join(assetDir, inputFilename);
156
+ return { videoPath };
157
+ }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
158
+ await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
159
+ type: "process_video",
160
+ videoPath: result.videoPath,
161
+ assetId,
162
+ assetDir,
163
+ bookId,
164
+ provenance: {
165
+ origin: "speed_change",
166
+ source_asset_id: assetId,
167
+ speed,
168
+ targetDuration,
169
+ speed_changed_at: new Date().toISOString(),
170
+ },
171
+ force: true,
172
+ skipDownstreamJobs: true,
173
+ });
174
+ return { success: true };
175
+ });
176
+ }