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,320 @@
1
+ import { RHYTHM_ANALYZER_ID, RHYTHM_ANALYZER_VERSION, RHYTHM_LATENCY_SAMPLES, RHYTHM_SCHEMA_VERSION, } from "./contract.js";
2
+ import { energyCurve } from "./energy.js";
3
+ import { beatPositions } from "./timing.js";
4
+ const MIN_BPM = 55;
5
+ const MAX_BPM = 200;
6
+ const TIGHTNESS = 80;
7
+ const LOW_CONFIDENCE = 0.42;
8
+ export function trackBeats(envelope) {
9
+ const hopSeconds = envelope.hopSamples / envelope.sampleRate;
10
+ const latencySeconds = RHYTHM_LATENCY_SAMPLES / envelope.sampleRate;
11
+ const onsets = pickOnsets(envelope.strength, hopSeconds);
12
+ const tempo = estimateTempo(envelope.strength, hopSeconds, onsets);
13
+ const tracked = tempo.periodHops === null
14
+ ? []
15
+ : beatTrack(envelope.strength, tempo.periodHops);
16
+ const beatTimes = tracked
17
+ .map((frame) => frame * hopSeconds)
18
+ .filter((time) => time >= 0);
19
+ const confidences = beatConfidences(envelope.strength, tracked);
20
+ const global = globalConfidence(confidences, envelope.strength, tracked);
21
+ const weakBeats = global < LOW_CONFIDENCE;
22
+ const noBeats = tempo.bpm === null || global <= 0 || beatTimes.length < (weakBeats ? 8 : 2);
23
+ const lowConfidence = noBeats || weakBeats || beatTimes.length < 8 || tempo.ambiguity !== "stable";
24
+ const beats = noBeats
25
+ ? []
26
+ : beatTimes.map((timeSeconds, index) => ({
27
+ timeSeconds,
28
+ confidence: confidences[index] ?? null,
29
+ }));
30
+ const positions = beatPositions(beats, tempo.bpm);
31
+ const detectedMeter = noBeats || weakBeats ? null : detectMeter(envelope.strength, tracked, positions);
32
+ const assignedMeter = detectedMeter ?? (noBeats ? null : { beatsPerBar: 4, confidence: 0, phase: 0 });
33
+ const meter = assignedMeter
34
+ ? { beatsPerBar: assignedMeter.beatsPerBar, confidence: assignedMeter.confidence }
35
+ : null;
36
+ const bars = assignedMeter ? barStarts(beats, positions, assignedMeter) : null;
37
+ return {
38
+ schemaVersion: RHYTHM_SCHEMA_VERSION,
39
+ analyzerId: RHYTHM_ANALYZER_ID,
40
+ analyzerVersion: RHYTHM_ANALYZER_VERSION,
41
+ sampleRate: envelope.sampleRate,
42
+ hopSamples: envelope.hopSamples,
43
+ windowSamples: envelope.windowSamples,
44
+ latencySeconds,
45
+ beats,
46
+ onsets,
47
+ bpm: noBeats ? null : tempo.bpm,
48
+ bpmConfidence: tempo.confidence,
49
+ tempoAmbiguity: tempo.ambiguity,
50
+ meter: noBeats ? null : meter,
51
+ bars: noBeats ? null : bars,
52
+ barSource: noBeats ? null : detectedMeter ? "detected" : "estimated",
53
+ noBeats,
54
+ lowConfidence,
55
+ energy: energyCurve(envelope),
56
+ };
57
+ }
58
+ function pickOnsets(strength, hopSeconds) {
59
+ if (strength.length < 3)
60
+ return [];
61
+ let mean = 0;
62
+ for (const value of strength)
63
+ mean += value;
64
+ mean /= strength.length;
65
+ let variance = 0;
66
+ for (const value of strength)
67
+ variance += (value - mean) ** 2;
68
+ const deviation = Math.sqrt(variance / strength.length);
69
+ const threshold = Math.max(0.08, mean + deviation * 0.4);
70
+ const minGap = Math.max(1, Math.round(0.05 / hopSeconds));
71
+ const onsets = [];
72
+ let last = -minGap;
73
+ for (let index = 1; index < strength.length - 1; index += 1) {
74
+ const value = strength[index];
75
+ if (value < threshold)
76
+ continue;
77
+ if (value < strength[index - 1] || value < strength[index + 1])
78
+ continue;
79
+ if (index - last < minGap) {
80
+ const previous = onsets[onsets.length - 1];
81
+ if (previous && value > previous.strength) {
82
+ onsets[onsets.length - 1] = { timeSeconds: index * hopSeconds, strength: value };
83
+ last = index;
84
+ }
85
+ continue;
86
+ }
87
+ onsets.push({ timeSeconds: index * hopSeconds, strength: value });
88
+ last = index;
89
+ }
90
+ return onsets;
91
+ }
92
+ function estimateTempo(strength, hopSeconds, onsets) {
93
+ const minLag = Math.max(2, Math.round(60 / MAX_BPM / hopSeconds));
94
+ const maxLag = Math.min(strength.length - 2, Math.round(60 / MIN_BPM / hopSeconds));
95
+ if (maxLag <= minLag + 1 || onsets.length < 2) {
96
+ return { bpm: null, confidence: null, ambiguity: null, periodHops: null };
97
+ }
98
+ const correlation = autocorrelate(strength, minLag, maxLag);
99
+ let bestLag = minLag;
100
+ let best = -Infinity;
101
+ for (let lag = minLag; lag <= maxLag; lag += 1) {
102
+ const value = correlation[lag - minLag];
103
+ if (value > best) {
104
+ best = value;
105
+ bestLag = lag;
106
+ }
107
+ }
108
+ const onsetPeriod = onsetInterval(onsets, hopSeconds, minLag, maxLag);
109
+ const resolved = resolveOctave(bestLag, correlation, minLag, onsetPeriod);
110
+ const bpm = 60 / (resolved.lag * hopSeconds);
111
+ const mean = correlation.reduce((sum, value) => sum + value, 0) / correlation.length;
112
+ const peakiness = best <= 0 ? 0 : (best - mean) / best;
113
+ const confidence = best <= 0 ? 0 : Math.max(0, Math.min(1, peakiness));
114
+ if (peakiness < 0.55) {
115
+ return { bpm: null, confidence, ambiguity: null, periodHops: null };
116
+ }
117
+ return {
118
+ bpm,
119
+ confidence,
120
+ ambiguity: resolved.ambiguity,
121
+ periodHops: resolved.lag,
122
+ };
123
+ }
124
+ function autocorrelate(strength, minLag, maxLag) {
125
+ const values = new Float64Array(maxLag - minLag + 1);
126
+ let energy = 0;
127
+ for (const value of strength)
128
+ energy += value * value;
129
+ const norm = Math.sqrt(energy) || 1;
130
+ for (let lag = minLag; lag <= maxLag; lag += 1) {
131
+ let sum = 0;
132
+ for (let index = 0; index + lag < strength.length; index += 1) {
133
+ sum += strength[index] * strength[index + lag];
134
+ }
135
+ values[lag - minLag] = sum / norm;
136
+ }
137
+ return values;
138
+ }
139
+ function onsetInterval(onsets, hopSeconds, minLag, maxLag) {
140
+ const gaps = [];
141
+ for (let index = 1; index < onsets.length; index += 1) {
142
+ const hops = Math.round((onsets[index].timeSeconds - onsets[index - 1].timeSeconds) / hopSeconds);
143
+ if (hops >= minLag && hops <= maxLag)
144
+ gaps.push(hops);
145
+ }
146
+ if (gaps.length === 0)
147
+ return null;
148
+ gaps.sort((left, right) => left - right);
149
+ return gaps[Math.floor(gaps.length / 2)];
150
+ }
151
+ function resolveOctave(lag, correlation, minLag, onsetPeriod) {
152
+ const candidates = [lag, Math.round(lag / 2), lag * 2].filter((value) => {
153
+ const index = value - minLag;
154
+ return value >= minLag && index >= 0 && index < correlation.length;
155
+ });
156
+ const unique = [...new Set(candidates)];
157
+ let chosen = lag;
158
+ let bestScore = -Infinity;
159
+ for (const candidate of unique) {
160
+ const auto = correlation[candidate - minLag] ?? 0;
161
+ const onsetScore = onsetPeriod === null
162
+ ? 0
163
+ : 1 - Math.min(1, Math.abs(candidate - onsetPeriod) / onsetPeriod);
164
+ const score = auto + onsetScore * Math.max(auto, 1);
165
+ if (score > bestScore) {
166
+ bestScore = score;
167
+ chosen = candidate;
168
+ }
169
+ }
170
+ let ambiguity = "stable";
171
+ const half = correlation[Math.round(chosen / 2) - minLag];
172
+ const double = correlation[chosen * 2 - minLag];
173
+ const self = correlation[chosen - minLag] ?? 0;
174
+ if (half !== undefined && self > 0 && half > self * 0.92)
175
+ ambiguity = "half";
176
+ else if (double !== undefined && self > 0 && double > self * 0.92)
177
+ ambiguity = "double";
178
+ if (chosen !== lag && chosen < lag)
179
+ ambiguity = "double";
180
+ if (chosen !== lag && chosen > lag)
181
+ ambiguity = "half";
182
+ return { lag: chosen, ambiguity };
183
+ }
184
+ function beatTrack(strength, period) {
185
+ const count = strength.length;
186
+ const score = new Float64Array(count);
187
+ const back = new Int32Array(count);
188
+ back.fill(-1);
189
+ const minLag = Math.max(1, Math.round(period * 0.55));
190
+ const maxLag = Math.max(minLag + 1, Math.round(period * 1.7));
191
+ for (let index = 0; index < count; index += 1) {
192
+ let best = strength[index];
193
+ let link = -1;
194
+ const start = Math.max(0, index - maxLag);
195
+ const end = index - minLag;
196
+ for (let previous = start; previous <= end; previous += 1) {
197
+ const lag = index - previous;
198
+ const penalty = TIGHTNESS * (Math.log(lag / period) ** 2);
199
+ const candidate = score[previous] + strength[index] - penalty;
200
+ if (candidate > best) {
201
+ best = candidate;
202
+ link = previous;
203
+ }
204
+ }
205
+ score[index] = link < 0 ? strength[index] : best;
206
+ back[index] = link;
207
+ }
208
+ let cursor = -1;
209
+ let bestScore = -Infinity;
210
+ const tail = Math.max(0, count - Math.round(period * 2));
211
+ for (let index = tail; index < count; index += 1) {
212
+ if (score[index] > bestScore) {
213
+ bestScore = score[index];
214
+ cursor = index;
215
+ }
216
+ }
217
+ const frames = [];
218
+ const guard = new Set();
219
+ while (cursor >= 0 && !guard.has(cursor)) {
220
+ guard.add(cursor);
221
+ if (strength[cursor] > 0.02)
222
+ frames.push(cursor);
223
+ cursor = back[cursor];
224
+ }
225
+ frames.reverse();
226
+ return thinBeats(frames, period);
227
+ }
228
+ function thinBeats(frames, period) {
229
+ const minimum = Math.max(1, Math.round(period * 0.55));
230
+ const kept = [];
231
+ for (const frame of frames) {
232
+ const previous = kept[kept.length - 1];
233
+ if (previous === undefined || frame - previous >= minimum)
234
+ kept.push(frame);
235
+ else
236
+ kept[kept.length - 1] = frame;
237
+ }
238
+ return kept;
239
+ }
240
+ function beatConfidences(strength, frames) {
241
+ return frames.map((frame) => {
242
+ const value = strength[frame] ?? 0;
243
+ return value > 0 ? Math.max(0, Math.min(1, value)) : null;
244
+ });
245
+ }
246
+ function globalConfidence(confidences, strength, frames) {
247
+ if (frames.length < 2)
248
+ return 0;
249
+ let sum = 0;
250
+ let known = 0;
251
+ for (const value of confidences) {
252
+ if (value === null)
253
+ continue;
254
+ sum += value;
255
+ known += 1;
256
+ }
257
+ if (known === 0)
258
+ return 0;
259
+ const mean = sum / known;
260
+ let noise = 0;
261
+ let samples = 0;
262
+ for (let index = 0; index < strength.length; index += 8) {
263
+ noise += strength[index];
264
+ samples += 1;
265
+ }
266
+ const floor = samples ? noise / samples : 0;
267
+ return Math.max(0, Math.min(1, mean - floor));
268
+ }
269
+ function detectMeter(strength, frames, positions) {
270
+ if (frames.length < 8)
271
+ return null;
272
+ let best = null;
273
+ for (const beatsPerBar of [3, 4, 5, 6]) {
274
+ if (frames.length < beatsPerBar * 4)
275
+ continue;
276
+ let bestPhase = 0;
277
+ let bestMean = -Infinity;
278
+ let other = 0;
279
+ for (let phase = 0; phase < beatsPerBar; phase += 1) {
280
+ let sum = 0;
281
+ let count = 0;
282
+ for (let index = 0; index < frames.length; index += 1) {
283
+ if (positions[index] % beatsPerBar !== phase)
284
+ continue;
285
+ sum += strength[frames[index]] ?? 0;
286
+ count += 1;
287
+ }
288
+ const mean = count ? sum / count : 0;
289
+ if (mean > bestMean) {
290
+ other = bestMean;
291
+ bestMean = mean;
292
+ bestPhase = phase;
293
+ }
294
+ else if (mean > other) {
295
+ other = mean;
296
+ }
297
+ }
298
+ const confidence = bestMean <= 0 ? 0 : (bestMean - Math.max(0, other)) / bestMean;
299
+ if (confidence < 0.18)
300
+ continue;
301
+ if (!best || confidence > best.confidence) {
302
+ best = { beatsPerBar, confidence, phase: bestPhase };
303
+ }
304
+ }
305
+ if (!best || best.confidence < 0.4)
306
+ return null;
307
+ return best;
308
+ }
309
+ function barStarts(beats, positions, meter) {
310
+ const bars = [];
311
+ for (let index = 0; index < beats.length; index += 1) {
312
+ if (positions[index] % meter.beatsPerBar !== meter.phase)
313
+ continue;
314
+ const beat = beats[index];
315
+ if (!beat)
316
+ continue;
317
+ bars.push({ timeSeconds: beat.timeSeconds, confidence: meter.confidence });
318
+ }
319
+ return bars.length ? bars : null;
320
+ }
@@ -0,0 +1,4 @@
1
+ export function fittedTransform(fit = "fit") {
2
+ return { fit, positionX: 0, positionY: 0, scaleX: 1, scaleY: 1, anchorX: 0.5, anchorY: 0.5,
3
+ rotationDegrees: 0, cropTop: 0, cropRight: 0, cropBottom: 0, cropLeft: 0, opacity: 1, blendMode: "normal" };
4
+ }
@@ -0,0 +1,25 @@
1
+ import { analyzeAudioFile } from "./analyze.js";
2
+ const controller = new AbortController();
3
+ process.once("SIGTERM", () => controller.abort());
4
+ process.once("disconnect", () => controller.abort());
5
+ async function send(message) {
6
+ if (!process.connected || !process.send)
7
+ return;
8
+ await new Promise((resolve, reject) => {
9
+ process.send(message, (error) => error ? reject(error) : resolve());
10
+ });
11
+ }
12
+ process.once("message", async (message) => {
13
+ try {
14
+ const analysis = await analyzeAudioFile(message.ffmpegPath, message.inputPath, controller.signal, message.streamIndex);
15
+ await send({ analysis });
16
+ }
17
+ catch (error) {
18
+ if (process.connected)
19
+ await send({ error: error instanceof Error ? error.message : String(error) });
20
+ }
21
+ finally {
22
+ if (process.connected)
23
+ process.disconnect?.();
24
+ }
25
+ });
@@ -0,0 +1,200 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { FFMPEG } from "./ffmpeg-binary.js";
4
+ const execFileAsync = promisify(execFile);
5
+ // Camera-cut threshold on scdet's absolute mean frame diff. ffmpeg's default
6
+ // scdet threshold is 10; we match that for the "shot" classification. Anything
7
+ // below this within a single shot is candidate territory for "settle".
8
+ const SHOT_SCORE_THRESHOLD = 10;
9
+ // scdet runs at this temporal resolution. 4 fps gives 0.25s candidate
10
+ // granularity — enough to land action endings cleanly while keeping the
11
+ // detection pass fast on long clips.
12
+ const SCDET_FPS = 4;
13
+ // Settle: within a single shot, look for the falling edge of motion. The
14
+ // frame must drop below the segment's 25th percentile after sustained motion
15
+ // above the 75th percentile, AND stay below p25 for at least this many
16
+ // seconds. Tuned so a short pause inside an action doesn't produce a false
17
+ // settle, but a real "the man finished standing up" landing does.
18
+ const SETTLE_HOLD_S = 0.5;
19
+ // Parse scdet's stderr stream. ffmpeg 6's scdet logs one line per processed
20
+ // frame in the form:
21
+ // [scdet @ 0x...] lavfi.scd.score: 4.798, lavfi.scd.time: 0.25
22
+ // Score is the absolute mean frame difference (Y-plane), time is seconds.
23
+ // The very first line always has score=0 (no previous frame) — we keep it
24
+ // in the timeline; deriveCandidates ignores it via the threshold.
25
+ function parseScdetOutput(stderr) {
26
+ const re = /lavfi\.scd\.score:\s*([0-9.]+),\s*lavfi\.scd\.time:\s*([0-9.]+)/g;
27
+ const out = [];
28
+ let m;
29
+ while ((m = re.exec(stderr)) !== null) {
30
+ const score = parseFloat(m[1]);
31
+ const t = parseFloat(m[2]);
32
+ if (Number.isFinite(score) && Number.isFinite(t))
33
+ out.push({ t, score });
34
+ }
35
+ return out;
36
+ }
37
+ export function deriveCameraCandidates(samples, scoreFloor = 6, clusterSeconds = 0.5) {
38
+ const peaks = samples.filter((sample, index) => {
39
+ if (sample.score < scoreFloor)
40
+ return false;
41
+ const previous = samples[index - 1]?.score ?? Number.NEGATIVE_INFINITY;
42
+ const next = samples[index + 1]?.score ?? Number.NEGATIVE_INFINITY;
43
+ return sample.score >= previous && sample.score >= next;
44
+ });
45
+ const clustered = [];
46
+ for (const peak of peaks) {
47
+ const previous = clustered.at(-1);
48
+ if (!previous || peak.t - previous.t > clusterSeconds) {
49
+ clustered.push({ ...peak });
50
+ continue;
51
+ }
52
+ if (peak.score > previous.score) {
53
+ clustered[clustered.length - 1] = { ...peak };
54
+ }
55
+ }
56
+ return clustered;
57
+ }
58
+ // Two-tier candidate derivation from a per-frame scdet timeline:
59
+ //
60
+ // 1. shot — any frame whose scdet score is ≥ SHOT_SCORE_THRESHOLD. These
61
+ // are camera/cut changes. ffmpeg's default scdet threshold is the
62
+ // same value; matching it keeps behavior aligned with the upstream
63
+ // definition of "scene change".
64
+ //
65
+ // 2. settle — within each run of consecutive non-shot frames (a single shot),
66
+ // find a falling edge: a frame where the score drops below the
67
+ // segment's 25th percentile after sustained motion above the 75th
68
+ // percentile, and stays below p25 for ≥ SETTLE_HOLD_S. The first
69
+ // frame of the sustained-low region is the action's natural end.
70
+ //
71
+ // Settle scoring is (above-mean − below-mean), so deeper drops rank higher.
72
+ // Empty / quiet shots produce no settle candidates.
73
+ function deriveCandidates(samples) {
74
+ if (samples.length === 0)
75
+ return [];
76
+ const out = [];
77
+ const shotIdx = [];
78
+ for (let i = 0; i < samples.length; i++) {
79
+ if (samples[i].score >= SHOT_SCORE_THRESHOLD) {
80
+ shotIdx.push(i);
81
+ out.push({ t: samples[i].t, score: samples[i].score, kind: "shot" });
82
+ }
83
+ }
84
+ const segments = [];
85
+ let segStart = 0;
86
+ for (const i of shotIdx) {
87
+ if (i > segStart)
88
+ segments.push([segStart, i]);
89
+ segStart = i + 1;
90
+ }
91
+ if (segStart < samples.length)
92
+ segments.push([segStart, samples.length]);
93
+ for (const [lo, hi] of segments) {
94
+ const seg = samples.slice(lo, hi);
95
+ if (seg.length < 3)
96
+ continue;
97
+ const sorted = [...seg.map((s) => s.score)].sort((a, b) => a - b);
98
+ const p25 = sorted[Math.floor(sorted.length * 0.25)];
99
+ const p75 = sorted[Math.floor(sorted.length * 0.75)];
100
+ if (p75 <= p25)
101
+ continue;
102
+ let aboveSum = 0;
103
+ let aboveCount = 0;
104
+ for (let j = 0; j < seg.length; j++) {
105
+ if (seg[j].score >= p75) {
106
+ aboveSum += seg[j].score;
107
+ aboveCount++;
108
+ }
109
+ if (aboveCount > 0 && seg[j].score <= p25) {
110
+ const start = seg[j].t;
111
+ let k = j;
112
+ while (k < seg.length && seg[k].score <= p25 && seg[k].t - start < SETTLE_HOLD_S)
113
+ k++;
114
+ if (k < seg.length && seg[k].t - start >= SETTLE_HOLD_S) {
115
+ let belowSum = 0;
116
+ let belowCount = 0;
117
+ for (let m = j; m < k; m++) {
118
+ belowSum += seg[m].score;
119
+ belowCount++;
120
+ }
121
+ const aboveMean = aboveSum / aboveCount;
122
+ const belowMean = belowCount > 0 ? belowSum / belowCount : 0;
123
+ out.push({ t: seg[j].t, score: Math.max(0, aboveMean - belowMean), kind: "settle" });
124
+ j = k;
125
+ aboveSum = 0;
126
+ aboveCount = 0;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ out.sort((a, b) => a.t - b.t);
132
+ return out;
133
+ }
134
+ // Run a single ffmpeg scdet pass over the input video at downscaled
135
+ // 320×180 / SCDET_FPS, parse the per-frame score timeline from stderr, and
136
+ // return shot + settle candidates. ffmpeg writes scdet's metadata to stderr
137
+ // (not stdout), so we capture stderr from the invocation. A non-zero exit is
138
+ // expected for `-f null -` runs only when ffmpeg actually fails — successful
139
+ // runs return cleanly with the metadata in stderr.
140
+ export async function detectSceneSamples(videoPath, options = {}) {
141
+ let stderr = "";
142
+ const startSeconds = Math.max(0, options.startSeconds ?? 0);
143
+ const args = [
144
+ "-hide_banner",
145
+ "-nostats",
146
+ "-loglevel", "info",
147
+ ];
148
+ if (startSeconds > 0)
149
+ args.push("-ss", String(startSeconds));
150
+ args.push("-i", videoPath);
151
+ if (typeof options.durationSeconds === "number"
152
+ && options.durationSeconds > 0) {
153
+ args.push("-t", String(options.durationSeconds));
154
+ }
155
+ const filters = ["scale=320:180"];
156
+ if (typeof options.fps === "number" && options.fps > 0) {
157
+ filters.push(`fps=${options.fps}`);
158
+ }
159
+ filters.push("scdet=threshold=0");
160
+ args.push("-vf", filters.join(","), "-an", "-f", "null", "-");
161
+ try {
162
+ const result = await execFileAsync(FFMPEG, args, {
163
+ maxBuffer: 64 * 1024 * 1024,
164
+ ...(options.signal ? { signal: options.signal } : {}),
165
+ });
166
+ stderr = result.stderr;
167
+ }
168
+ catch (error) {
169
+ if (options.signal?.aborted) {
170
+ throw options.signal.reason instanceof Error
171
+ ? options.signal.reason
172
+ : new Error("Scene detection was cancelled");
173
+ }
174
+ const e = error;
175
+ throw new Error(`scdet pass failed: ${e.message}`);
176
+ }
177
+ return parseScdetOutput(stderr).map((sample) => ({
178
+ ...sample,
179
+ t: sample.t + startSeconds,
180
+ }));
181
+ }
182
+ export async function refineCameraCandidate(videoPath, candidate, signal) {
183
+ const startSeconds = Math.max(0, candidate.t - 0.75);
184
+ const samples = await detectSceneSamples(videoPath, {
185
+ startSeconds,
186
+ durationSeconds: 1.5,
187
+ signal,
188
+ });
189
+ const best = samples
190
+ .filter((sample) => Math.abs(sample.t - candidate.t) <= 0.75)
191
+ .sort((left, right) => right.score - left.score)[0];
192
+ return best ? { t: best.t, score: best.score } : candidate;
193
+ }
194
+ export async function detectSceneCandidates(videoPath, signal) {
195
+ const samples = await detectSceneSamples(videoPath, {
196
+ fps: SCDET_FPS,
197
+ signal,
198
+ });
199
+ return deriveCandidates(samples);
200
+ }
@@ -0,0 +1,12 @@
1
+ export const MIN_TRANSITION_SECONDS = 0.05;
2
+ export function fixedTransitionSeconds(firstDuration, secondDuration, requested) {
3
+ const available = Math.min(firstDuration, secondDuration);
4
+ const transition = requested ?? Math.max(MIN_TRANSITION_SECONDS, Math.min(0.35, available / 2));
5
+ if (!Number.isFinite(transition) || transition < MIN_TRANSITION_SECONDS) {
6
+ throw new Error(`Transition must be at least ${MIN_TRANSITION_SECONDS} seconds`);
7
+ }
8
+ if (!Number.isFinite(available) || transition >= available) {
9
+ throw new Error("Transition must be shorter than both video clips");
10
+ }
11
+ return transition;
12
+ }