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,941 @@
1
+ import { createOpenAi } from "./usage-providers.js";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { findAudioFile, } from "videobook-engine";
7
+ import { getApiKey } from "./api-key.js";
8
+ import { runFfmpeg } from "./ffmpeg.js";
9
+ import { materializeArtifactFile } from "./notebook-media-ops.js";
10
+ import { MAX_SHOT_SECONDS } from "./notebook-shots.js";
11
+ import { ANALYSIS_REASONING_EFFORT, INTELLIGENCE_MODEL, buildIntelligenceResponses, firstResponseText, intelligenceRequest, } from "./openai-intelligence.js";
12
+ import { deriveCameraCandidates, detectSceneSamples, refineCameraCandidate, } from "./scene-detect.js";
13
+ const MAX_ANALYSIS_FRAMES = 15;
14
+ const CAMERA_BATCH_SIZE = 6;
15
+ const CAMERA_BATCH_CONCURRENCY = 2;
16
+ const CAMERA_ACCEPT_CONFIDENCE = 0.75;
17
+ const CAMERA_RECHECK_CONFIDENCE = 0.6;
18
+ const AUDIO_TARGET_WINDOW_SECONDS = 3;
19
+ const AUDIO_BOUNDARY_MIN_SPACING_SECONDS = 4;
20
+ const CAMERA_FRAME_OFFSETS = [-0.5, -0.1, 0.1, 0.5];
21
+ const CAMERA_VALIDATION_JSON_SCHEMA = {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ decisions: {
26
+ type: "array",
27
+ items: {
28
+ type: "object",
29
+ additionalProperties: false,
30
+ properties: {
31
+ candidateId: { type: "string", minLength: 1 },
32
+ cameraSwitch: { type: "boolean" },
33
+ transitionType: {
34
+ type: "string",
35
+ enum: ["hard_cut", "dissolve", "false_positive"],
36
+ },
37
+ confidence: { type: "number", minimum: 0, maximum: 1 },
38
+ },
39
+ required: [
40
+ "candidateId",
41
+ "cameraSwitch",
42
+ "transitionType",
43
+ "confidence",
44
+ ],
45
+ },
46
+ },
47
+ },
48
+ required: ["decisions"],
49
+ };
50
+ const AUDIO_SELECTION_JSON_SCHEMA = {
51
+ type: "object",
52
+ additionalProperties: false,
53
+ properties: {
54
+ selections: {
55
+ type: "array",
56
+ items: {
57
+ type: "object",
58
+ additionalProperties: false,
59
+ properties: {
60
+ targetId: { type: "string", minLength: 1 },
61
+ candidateId: { type: ["string", "null"] },
62
+ },
63
+ required: ["targetId", "candidateId"],
64
+ },
65
+ },
66
+ },
67
+ required: ["selections"],
68
+ };
69
+ const SHOT_LABEL_JSON_SCHEMA = {
70
+ type: "object",
71
+ additionalProperties: false,
72
+ properties: {
73
+ shots: {
74
+ type: "array",
75
+ minItems: 1,
76
+ items: {
77
+ type: "object",
78
+ additionalProperties: false,
79
+ properties: {
80
+ shotId: { type: "string", minLength: 1 },
81
+ label: { type: "string", minLength: 1 },
82
+ prompt: { type: "string", minLength: 1 },
83
+ },
84
+ required: ["shotId", "label", "prompt"],
85
+ },
86
+ },
87
+ },
88
+ required: ["shots"],
89
+ };
90
+ const CameraValidationSchema = z.object({
91
+ decisions: z.array(z.object({
92
+ candidateId: z.string().min(1),
93
+ cameraSwitch: z.boolean(),
94
+ transitionType: z.enum(["hard_cut", "dissolve", "false_positive"]),
95
+ confidence: z.number().min(0).max(1),
96
+ }).strict()),
97
+ }).strict();
98
+ const AudioSelectionSchema = z.object({
99
+ selections: z.array(z.object({
100
+ targetId: z.string().min(1),
101
+ candidateId: z.string().min(1).nullable(),
102
+ }).strict()),
103
+ }).strict();
104
+ const ShotLabelSchema = z.object({
105
+ shots: z.array(z.object({
106
+ shotId: z.string().min(1),
107
+ label: z.string().min(1),
108
+ prompt: z.string().min(1),
109
+ }).strict()).min(1),
110
+ }).strict();
111
+ function roundTime(value) {
112
+ return Math.round(value * 1000) / 1000;
113
+ }
114
+ function frameTimestamp(name) {
115
+ const value = Number.parseFloat(name);
116
+ return Number.isFinite(value) && value >= 0 ? value : null;
117
+ }
118
+ function frameMime(name) {
119
+ const extension = name.split(".").at(-1)?.toLowerCase();
120
+ if (extension === "png")
121
+ return "image/png";
122
+ if (extension === "webp")
123
+ return "image/webp";
124
+ return "image/jpeg";
125
+ }
126
+ function decode(value) {
127
+ if (typeof value === "string")
128
+ return value;
129
+ return new TextDecoder().decode(value);
130
+ }
131
+ function transcriptText(value) {
132
+ if (!value || typeof value !== "object" || Array.isArray(value))
133
+ return null;
134
+ const text = value.text;
135
+ return typeof text === "string" && text.trim() ? text.trim() : null;
136
+ }
137
+ function audioMime(name) {
138
+ const extension = name.split(".").at(-1)?.toLowerCase();
139
+ if (extension === "mp3")
140
+ return "audio/mpeg";
141
+ if (extension === "wav")
142
+ return "audio/wav";
143
+ if (extension === "m4a")
144
+ return "audio/mp4";
145
+ if (extension === "webm" || extension === "weba")
146
+ return "audio/webm";
147
+ return "application/octet-stream";
148
+ }
149
+ function sourceSeconds(tick, timeBase) {
150
+ return tick * timeBase.numerator / timeBase.denominator;
151
+ }
152
+ async function readTranscript(engine, sourceArtifactId) {
153
+ const listed = await engine.transcripts.list(sourceArtifactId);
154
+ if (listed.ok) {
155
+ const transcript = [...listed.value]
156
+ .reverse()
157
+ .find((candidate) => candidate.payloadAvailable);
158
+ if (transcript) {
159
+ const text = transcript.segments
160
+ .map((segment) => segment.text.trim())
161
+ .filter(Boolean)
162
+ .join(" ");
163
+ return {
164
+ text: text || null,
165
+ timedTranscript: transcript,
166
+ ...(transcript.provider ? { provider: transcript.provider } : {}),
167
+ ...(transcript.model ? { model: transcript.model } : {}),
168
+ transcriptId: transcript.transcriptId,
169
+ };
170
+ }
171
+ }
172
+ for (const filename of ["elevenlabs.json", ".analysis.gpt4otrans.json"]) {
173
+ const read = await engine.files.read(sourceArtifactId, filename);
174
+ if (!read.ok)
175
+ continue;
176
+ try {
177
+ const text = transcriptText(JSON.parse(decode(read.value)));
178
+ if (text)
179
+ return { text, timedTranscript: null };
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ }
185
+ return { text: null, timedTranscript: null };
186
+ }
187
+ async function transcribeMissingText(engine, sourceArtifactId, transcript, signal) {
188
+ if (transcript.text)
189
+ return transcript;
190
+ const apiKey = getApiKey("OPENAI_API_KEY");
191
+ if (!apiKey)
192
+ return transcript;
193
+ const manifest = await engine.files.manifest(sourceArtifactId);
194
+ if (!manifest.ok)
195
+ return transcript;
196
+ const artifact = engine.artifacts.get(sourceArtifactId);
197
+ if (!artifact.ok)
198
+ return transcript;
199
+ const audio = findAudioFile(manifest.value.files, artifact.value.kind)
200
+ ?? manifest.value.files.find((file) => /\.(?:mp3|wav|m4a|aac|ogg|flac|weba)$/i.test(file.name));
201
+ if (!audio)
202
+ return transcript;
203
+ const read = await engine.files.read(sourceArtifactId, audio.name);
204
+ if (!read.ok || read.value.byteLength > 24 * 1024 * 1024)
205
+ return transcript;
206
+ try {
207
+ const provider = createOpenAi({ apiKey, timeout: 120_000 });
208
+ const file = new File([new Uint8Array(read.value)], audio.name, { type: audioMime(audio.name) });
209
+ const result = await provider.post.v1.audio.transcriptions({
210
+ file,
211
+ model: "gpt-transcribe",
212
+ }, signal);
213
+ const text = result.text.trim();
214
+ if (!text)
215
+ return transcript;
216
+ await engine.files.write(sourceArtifactId, ".analysis.gpt4otrans.json", JSON.stringify({
217
+ text,
218
+ provider: "openai",
219
+ model: "gpt-transcribe",
220
+ }, null, 2));
221
+ return {
222
+ ...transcript,
223
+ text,
224
+ provider: "openai",
225
+ model: "gpt-transcribe",
226
+ };
227
+ }
228
+ catch {
229
+ return transcript;
230
+ }
231
+ }
232
+ async function readDuration(engine, sourceArtifactId, configuredDuration) {
233
+ if (typeof configuredDuration === "number" && configuredDuration > 0) {
234
+ return configuredDuration;
235
+ }
236
+ const metadata = await engine.metadata.artifacts.read(sourceArtifactId, "original");
237
+ const duration = metadata.ok ? metadata.value.duration : undefined;
238
+ if (typeof duration !== "number"
239
+ || !Number.isFinite(duration)
240
+ || duration <= 0) {
241
+ throw new Error(`Analyze source ${sourceArtifactId} requires a positive source duration; process the video or set sourceDurationSeconds`);
242
+ }
243
+ return duration;
244
+ }
245
+ export function sampleNotebookAnalysisFrames(names, limit = MAX_ANALYSIS_FRAMES) {
246
+ const chronological = names
247
+ .flatMap((name) => {
248
+ if (!/\.(?:jpe?g|png|webp)$/i.test(name))
249
+ return [];
250
+ const timestampSeconds = frameTimestamp(name);
251
+ return timestampSeconds === null ? [] : [{ name, timestampSeconds }];
252
+ })
253
+ .sort((left, right) => left.timestampSeconds - right.timestampSeconds
254
+ || left.name.localeCompare(right.name));
255
+ if (chronological.length <= limit)
256
+ return chronological;
257
+ const step = (chronological.length - 1) / (limit - 1);
258
+ return Array.from({ length: limit }, (_, index) => chronological[Math.round(index * step)]);
259
+ }
260
+ async function readFrames(engine, sourceArtifactId) {
261
+ const listed = await engine.files.listSubdir(sourceArtifactId, "original_frames");
262
+ if (!listed.ok) {
263
+ throw new Error(`Analyze source ${sourceArtifactId} requires processed original_frames; run video processing first`);
264
+ }
265
+ const sampled = sampleNotebookAnalysisFrames(listed.value);
266
+ if (sampled.length === 0) {
267
+ throw new Error(`Analyze source ${sourceArtifactId} requires processed original_frames with at least one timestamped image; run video processing first`);
268
+ }
269
+ const frames = [];
270
+ for (const frame of sampled) {
271
+ const read = await engine.files.read(sourceArtifactId, `original_frames/${frame.name}`);
272
+ if (!read.ok)
273
+ continue;
274
+ const bytes = Buffer.from(read.value);
275
+ frames.push({
276
+ ...frame,
277
+ dataUrl: `data:${frameMime(frame.name)};base64,${bytes.toString("base64")}`,
278
+ });
279
+ }
280
+ if (frames.length === 0) {
281
+ throw new Error(`Analyze source ${sourceArtifactId} could not read any sampled original_frames`);
282
+ }
283
+ return frames;
284
+ }
285
+ async function mapConcurrent(values, concurrency, operation) {
286
+ const output = new Array(values.length);
287
+ let cursor = 0;
288
+ const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
289
+ while (cursor < values.length) {
290
+ const index = cursor;
291
+ cursor += 1;
292
+ output[index] = await operation(values[index], index);
293
+ }
294
+ });
295
+ await Promise.all(workers);
296
+ return output;
297
+ }
298
+ function clusterCandidates(candidates, clusterSeconds = 0.25) {
299
+ const clustered = [];
300
+ for (const candidate of [...candidates].sort((left, right) => left.t - right.t)) {
301
+ const previous = clustered.at(-1);
302
+ if (!previous || candidate.t - previous.t > clusterSeconds) {
303
+ clustered.push(candidate);
304
+ continue;
305
+ }
306
+ if (candidate.score > previous.score) {
307
+ clustered[clustered.length - 1] = candidate;
308
+ }
309
+ }
310
+ return clustered;
311
+ }
312
+ async function extractCandidateFrame(videoPath, timestampSeconds, offsetSeconds, duration, signal) {
313
+ const timestamp = Math.max(0, Math.min(Math.max(0, duration - 0.001), timestampSeconds + offsetSeconds));
314
+ const directory = await mkdtemp(join(tmpdir(), "vb-analysis-frame-"));
315
+ const outputPath = join(directory, "frame.jpg");
316
+ try {
317
+ await runFfmpeg([
318
+ "-y",
319
+ "-ss",
320
+ String(timestamp),
321
+ "-i",
322
+ videoPath,
323
+ "-frames:v",
324
+ "1",
325
+ "-vf",
326
+ "scale=1024:-2:force_original_aspect_ratio=decrease",
327
+ "-q:v",
328
+ "2",
329
+ outputPath,
330
+ ], { signal });
331
+ const bytes = await readFile(outputPath);
332
+ return {
333
+ timestampSeconds: roundTime(timestamp),
334
+ offsetSeconds,
335
+ dataUrl: `data:image/jpeg;base64,${bytes.toString("base64")}`,
336
+ };
337
+ }
338
+ finally {
339
+ await rm(directory, { recursive: true, force: true });
340
+ }
341
+ }
342
+ async function collectVisualEvidence(engine, sourceArtifactId, duration, signal) {
343
+ const source = await materializeArtifactFile(engine, sourceArtifactId, [
344
+ "original.mp4",
345
+ "original.mov",
346
+ ]);
347
+ try {
348
+ const samples = await detectSceneSamples(source.path, {
349
+ fps: 4,
350
+ signal,
351
+ });
352
+ const coarse = deriveCameraCandidates(samples);
353
+ const refined = await mapConcurrent(coarse, CAMERA_BATCH_CONCURRENCY, (candidate) => refineCameraCandidate(source.path, candidate, signal));
354
+ const candidates = clusterCandidates(refined)
355
+ .filter((candidate) => candidate.t > 0.05 && candidate.t < duration - 0.05);
356
+ return await mapConcurrent(candidates, CAMERA_BATCH_CONCURRENCY, async (candidate, index) => {
357
+ const frames = await mapConcurrent([...CAMERA_FRAME_OFFSETS], 2, (offset) => extractCandidateFrame(source.path, candidate.t, offset, duration, signal));
358
+ return {
359
+ candidate: {
360
+ id: `visual-${String(index + 1).padStart(3, "0")}`,
361
+ timestampSeconds: roundTime(candidate.t),
362
+ score: roundTime(candidate.score),
363
+ frameTimestamps: frames.map((frame) => frame.timestampSeconds),
364
+ },
365
+ frames,
366
+ };
367
+ });
368
+ }
369
+ finally {
370
+ await source.cleanup();
371
+ }
372
+ }
373
+ function suppliedVisualEvidence(evidence) {
374
+ return evidence
375
+ .filter((candidate) => Number.isFinite(candidate.timestampSeconds)
376
+ && candidate.timestampSeconds > 0)
377
+ .sort((left, right) => left.timestampSeconds - right.timestampSeconds)
378
+ .map((candidate, index) => ({
379
+ candidate: {
380
+ id: candidate.id ?? `visual-${String(index + 1).padStart(3, "0")}`,
381
+ timestampSeconds: roundTime(candidate.timestampSeconds),
382
+ score: roundTime(candidate.score),
383
+ frameTimestamps: candidate.frames.map((frame) => roundTime(frame.timestampSeconds)),
384
+ },
385
+ frames: candidate.frames,
386
+ }));
387
+ }
388
+ function cameraValidationContent(candidates, recheck) {
389
+ const content = [{
390
+ type: "input_text",
391
+ text: recheck
392
+ ? "Recheck each ambiguous candidate using all four frames. A camera switch changes the editorial shot or camera view; reject subject motion, zooms, pans, flashes, overlays, and compression artifacts."
393
+ : "Verify whether each candidate is a real editorial camera switch. Use the narrow before/after pair first. Reject subject motion, zooms, pans, flashes, overlays, and compression artifacts.",
394
+ }];
395
+ for (const entry of candidates) {
396
+ content.push({
397
+ type: "input_text",
398
+ text: `Candidate ${entry.candidate.id} at ${entry.candidate.timestampSeconds.toFixed(3)} seconds; local score ${entry.candidate.score.toFixed(3)}.`,
399
+ });
400
+ const frames = recheck
401
+ ? entry.frames
402
+ : entry.frames.filter((frame) => Math.abs(frame.offsetSeconds) <= 0.11);
403
+ for (const frame of frames) {
404
+ content.push({
405
+ type: "input_text",
406
+ text: `Candidate ${entry.candidate.id}, offset ${frame.offsetSeconds.toFixed(1)} seconds, source time ${frame.timestampSeconds.toFixed(3)} seconds.`,
407
+ });
408
+ content.push({
409
+ type: "input_image",
410
+ image_url: frame.dataUrl,
411
+ detail: "high",
412
+ });
413
+ }
414
+ }
415
+ return content;
416
+ }
417
+ async function requestCameraDecisions(responses, candidates, recheck, signal) {
418
+ const batches = Array.from({ length: Math.ceil(candidates.length / CAMERA_BATCH_SIZE) }, (_, index) => candidates.slice(index * CAMERA_BATCH_SIZE, (index + 1) * CAMERA_BATCH_SIZE));
419
+ const outputs = await mapConcurrent(batches, CAMERA_BATCH_CONCURRENCY, async (batch) => {
420
+ const response = await responses(intelligenceRequest({
421
+ instructions: "Return one decision for every supplied candidate ID. Do not add candidate IDs or timestamps.",
422
+ input: [{
423
+ role: "user",
424
+ content: cameraValidationContent(batch, recheck),
425
+ }],
426
+ text: {
427
+ format: {
428
+ type: "json_schema",
429
+ name: "notebook_camera_cut_validation",
430
+ schema: CAMERA_VALIDATION_JSON_SCHEMA,
431
+ strict: true,
432
+ },
433
+ },
434
+ max_output_tokens: 2048,
435
+ }, { reasoningEffort: ANALYSIS_REASONING_EFFORT }), signal);
436
+ return CameraValidationSchema.parse(JSON.parse(firstResponseText(response))).decisions;
437
+ });
438
+ return outputs.flat();
439
+ }
440
+ async function validateCameraCandidates(responses, candidates, signal) {
441
+ if (candidates.length === 0)
442
+ return [];
443
+ const initial = await requestCameraDecisions(responses, candidates, false, signal);
444
+ const initialById = new Map(initial.map((decision) => [
445
+ decision.candidateId,
446
+ decision,
447
+ ]));
448
+ const ambiguous = candidates.filter((candidate) => {
449
+ const decision = initialById.get(candidate.candidate.id);
450
+ return !decision || decision.confidence < CAMERA_ACCEPT_CONFIDENCE;
451
+ });
452
+ const rechecked = ambiguous.length > 0
453
+ ? await requestCameraDecisions(responses, ambiguous, true, signal)
454
+ : [];
455
+ const recheckedById = new Map(rechecked.map((decision) => [
456
+ decision.candidateId,
457
+ decision,
458
+ ]));
459
+ return candidates.map((candidate) => {
460
+ const first = initialById.get(candidate.candidate.id);
461
+ const second = recheckedById.get(candidate.candidate.id);
462
+ const decision = second ?? first;
463
+ const threshold = second
464
+ ? CAMERA_RECHECK_CONFIDENCE
465
+ : CAMERA_ACCEPT_CONFIDENCE;
466
+ const accepted = decision?.cameraSwitch === true
467
+ && decision.confidence >= threshold;
468
+ return {
469
+ candidateId: candidate.candidate.id,
470
+ accepted,
471
+ cameraSwitch: decision?.cameraSwitch ?? false,
472
+ transitionType: accepted
473
+ ? decision?.transitionType === "dissolve"
474
+ ? "dissolve"
475
+ : "hard_cut"
476
+ : "false_positive",
477
+ confidence: roundTime(decision?.confidence ?? 0),
478
+ rechecked: second !== undefined,
479
+ };
480
+ });
481
+ }
482
+ function audioCandidateDrafts(transcript, waveform, duration) {
483
+ const candidates = [];
484
+ if (transcript) {
485
+ const speechSegments = transcript.segments
486
+ .filter((segment) => segment.kind === "speech")
487
+ .sort((left, right) => sourceSeconds(left.range.startTick, left.range.timeBase)
488
+ - sourceSeconds(right.range.startTick, right.range.timeBase));
489
+ const words = speechSegments
490
+ .flatMap((segment) => segment.words.map((word) => ({
491
+ speaker: segment.speaker,
492
+ text: word.text.trim(),
493
+ startSeconds: sourceSeconds(word.startTick, segment.range.timeBase),
494
+ endSeconds: sourceSeconds(word.startTick + word.durationTicks, segment.range.timeBase),
495
+ })))
496
+ .sort((left, right) => left.startSeconds - right.startSeconds);
497
+ for (let index = 1; index < words.length; index += 1) {
498
+ const previous = words[index - 1];
499
+ const current = words[index];
500
+ const gap = current.startSeconds - previous.endSeconds;
501
+ if (gap >= 0.75) {
502
+ candidates.push({
503
+ timestampSeconds: roundTime(previous.endSeconds + gap / 2),
504
+ kind: "pause",
505
+ score: Math.min(1, gap / 2),
506
+ detail: `${gap.toFixed(2)} second pause between "${previous.text}" and "${current.text}"`,
507
+ });
508
+ }
509
+ if (previous.speaker
510
+ && current.speaker
511
+ && previous.speaker !== current.speaker) {
512
+ candidates.push({
513
+ timestampSeconds: roundTime(current.startSeconds),
514
+ kind: "speaker_change",
515
+ score: 0.8,
516
+ detail: `Speaker change before "${current.text}"`,
517
+ });
518
+ }
519
+ }
520
+ for (const segment of transcript.segments) {
521
+ if (segment.kind === "speech" || segment.kind === "other")
522
+ continue;
523
+ const start = sourceSeconds(segment.range.startTick, segment.range.timeBase);
524
+ const end = sourceSeconds(segment.range.startTick + segment.range.durationTicks, segment.range.timeBase);
525
+ const kind = segment.kind;
526
+ const score = kind === "silence" ? 0.9 : kind === "music" ? 0.75 : 0.65;
527
+ candidates.push({
528
+ timestampSeconds: roundTime(start),
529
+ kind,
530
+ score,
531
+ detail: `${kind} begins`,
532
+ });
533
+ candidates.push({
534
+ timestampSeconds: roundTime(end),
535
+ kind,
536
+ score,
537
+ detail: `${kind} ends`,
538
+ });
539
+ }
540
+ }
541
+ if (waveform.length > 2 && duration > 0) {
542
+ const secondsPerPeak = duration / waveform.length;
543
+ const radius = Math.max(1, Math.round(0.5 / secondsPerPeak));
544
+ for (let index = 1; index < waveform.length - 1; index += 1) {
545
+ const value = waveform[index];
546
+ const start = Math.max(0, index - radius);
547
+ const end = Math.min(waveform.length, index + radius + 1);
548
+ const window = waveform.slice(start, end);
549
+ if (value <= 0.15 && value === Math.min(...window)) {
550
+ candidates.push({
551
+ timestampSeconds: roundTime((index + 0.5) * secondsPerPeak),
552
+ kind: "waveform_minimum",
553
+ score: 1 - value,
554
+ detail: "Local audio-energy minimum",
555
+ });
556
+ }
557
+ const delta = Math.abs(value - waveform[index - 1]);
558
+ if (delta >= 0.35) {
559
+ candidates.push({
560
+ timestampSeconds: roundTime((index + 0.5) * secondsPerPeak),
561
+ kind: "level_change",
562
+ score: Math.min(1, delta),
563
+ detail: "Abrupt audio-level change",
564
+ });
565
+ }
566
+ }
567
+ }
568
+ return candidates;
569
+ }
570
+ export function buildAudioCandidates(transcript, waveform, duration) {
571
+ const drafts = audioCandidateDrafts(transcript, waveform, duration)
572
+ .filter((candidate) => candidate.timestampSeconds > 0.25
573
+ && candidate.timestampSeconds < duration - 0.25)
574
+ .sort((left, right) => left.timestampSeconds - right.timestampSeconds
575
+ || right.score - left.score);
576
+ const deduplicated = [];
577
+ for (const candidate of drafts) {
578
+ const previous = deduplicated.at(-1);
579
+ if (!previous
580
+ || candidate.timestampSeconds - previous.timestampSeconds > 0.5) {
581
+ deduplicated.push(candidate);
582
+ continue;
583
+ }
584
+ if (candidate.score > previous.score) {
585
+ deduplicated[deduplicated.length - 1] = candidate;
586
+ }
587
+ }
588
+ return deduplicated.map((candidate, index) => ({
589
+ id: `audio-${String(index + 1).padStart(3, "0")}`,
590
+ ...candidate,
591
+ timestampSeconds: roundTime(candidate.timestampSeconds),
592
+ score: roundTime(candidate.score),
593
+ }));
594
+ }
595
+ function buildBoundaryTargets(cameraBoundaries, audioCandidates, duration) {
596
+ const visualTimes = [
597
+ 0,
598
+ ...cameraBoundaries.map((boundary) => boundary.timestampSeconds),
599
+ duration,
600
+ ];
601
+ const targets = [];
602
+ for (let intervalIndex = 0; intervalIndex < visualTimes.length - 1; intervalIndex += 1) {
603
+ const start = visualTimes[intervalIndex];
604
+ const end = visualTimes[intervalIndex + 1];
605
+ const intervalDuration = end - start;
606
+ const partCount = Math.max(1, Math.ceil(intervalDuration / MAX_SHOT_SECONDS));
607
+ const intervalId = `interval-${String(intervalIndex + 1).padStart(3, "0")}`;
608
+ for (let part = 1; part < partCount; part += 1) {
609
+ const idealSeconds = roundTime(start + intervalDuration * part / partCount);
610
+ const optionIds = audioCandidates
611
+ .filter((candidate) => candidate.timestampSeconds >= start + AUDIO_BOUNDARY_MIN_SPACING_SECONDS
612
+ && candidate.timestampSeconds <= end - AUDIO_BOUNDARY_MIN_SPACING_SECONDS
613
+ && Math.abs(candidate.timestampSeconds - idealSeconds)
614
+ <= AUDIO_TARGET_WINDOW_SECONDS)
615
+ .sort((left, right) => right.score - left.score
616
+ || Math.abs(left.timestampSeconds - idealSeconds)
617
+ - Math.abs(right.timestampSeconds - idealSeconds))
618
+ .slice(0, 12)
619
+ .map((candidate) => candidate.id);
620
+ targets.push({
621
+ id: `${intervalId}-target-${String(part).padStart(2, "0")}`,
622
+ intervalId,
623
+ idealSeconds,
624
+ optionIds,
625
+ });
626
+ }
627
+ }
628
+ return targets;
629
+ }
630
+ async function selectAudioCandidates(responses, targets, audioCandidates, signal) {
631
+ const selectable = targets.filter((target) => target.optionIds.length > 0);
632
+ if (selectable.length === 0)
633
+ return new Map();
634
+ const optionIds = new Set(selectable.flatMap((target) => target.optionIds));
635
+ const options = audioCandidates.filter((candidate) => optionIds.has(candidate.id));
636
+ const response = await responses(intelligenceRequest({
637
+ instructions: [
638
+ "Select at most one supplied audio candidate for each target.",
639
+ "Prefer clean pauses, speaker or music transitions, then strong waveform changes.",
640
+ "Return null when no option is a credible editorial boundary.",
641
+ "Do not add IDs or timestamps.",
642
+ ].join("\n"),
643
+ input: [{
644
+ role: "user",
645
+ content: [{
646
+ type: "input_text",
647
+ text: JSON.stringify({ targets: selectable, candidates: options }),
648
+ }],
649
+ }],
650
+ text: {
651
+ format: {
652
+ type: "json_schema",
653
+ name: "notebook_audio_boundary_selection",
654
+ schema: AUDIO_SELECTION_JSON_SCHEMA,
655
+ strict: true,
656
+ },
657
+ },
658
+ max_output_tokens: 2048,
659
+ }, { reasoningEffort: ANALYSIS_REASONING_EFFORT }), signal);
660
+ const parsed = AudioSelectionSchema.parse(JSON.parse(firstResponseText(response)));
661
+ return new Map(parsed.selections.map((selection) => [
662
+ selection.targetId,
663
+ selection.candidateId,
664
+ ]));
665
+ }
666
+ function selectedAudioBoundaries(targets, selections, audioCandidates, cameraBoundaries, duration) {
667
+ const candidateById = new Map(audioCandidates.map((candidate) => [
668
+ candidate.id,
669
+ candidate,
670
+ ]));
671
+ const targetsByInterval = new Map();
672
+ for (const target of targets) {
673
+ const entries = targetsByInterval.get(target.intervalId) ?? [];
674
+ entries.push(target);
675
+ targetsByInterval.set(target.intervalId, entries);
676
+ }
677
+ const visualTimes = [
678
+ 0,
679
+ ...cameraBoundaries.map((boundary) => boundary.timestampSeconds),
680
+ duration,
681
+ ];
682
+ const output = [];
683
+ for (let intervalIndex = 0; intervalIndex < visualTimes.length - 1; intervalIndex += 1) {
684
+ const intervalId = `interval-${String(intervalIndex + 1).padStart(3, "0")}`;
685
+ const intervalTargets = targetsByInterval.get(intervalId) ?? [];
686
+ if (intervalTargets.length === 0)
687
+ continue;
688
+ const start = visualTimes[intervalIndex];
689
+ const end = visualTimes[intervalIndex + 1];
690
+ const proposed = intervalTargets.map((target) => {
691
+ const selectedId = selections.get(target.id);
692
+ const selected = selectedId && target.optionIds.includes(selectedId)
693
+ ? candidateById.get(selectedId)
694
+ : undefined;
695
+ return selected
696
+ ? {
697
+ target,
698
+ timestampSeconds: selected.timestampSeconds,
699
+ candidate: selected,
700
+ }
701
+ : {
702
+ target,
703
+ timestampSeconds: target.idealSeconds,
704
+ candidate: undefined,
705
+ };
706
+ }).sort((left, right) => left.timestampSeconds - right.timestampSeconds);
707
+ const times = [start, ...proposed.map((entry) => entry.timestampSeconds), end];
708
+ const valid = times.every((time, index) => {
709
+ if (index === 0)
710
+ return true;
711
+ const spacing = time - times[index - 1];
712
+ return spacing >= AUDIO_BOUNDARY_MIN_SPACING_SECONDS
713
+ && spacing <= MAX_SHOT_SECONDS + 0.001;
714
+ });
715
+ const entries = valid
716
+ ? proposed
717
+ : intervalTargets.map((target) => ({
718
+ target,
719
+ timestampSeconds: target.idealSeconds,
720
+ candidate: undefined,
721
+ }));
722
+ for (const entry of entries) {
723
+ output.push(entry.candidate
724
+ ? {
725
+ id: `boundary-${entry.candidate.id}`,
726
+ timestampSeconds: entry.candidate.timestampSeconds,
727
+ kind: "audio_transition",
728
+ confidence: entry.candidate.score,
729
+ evidenceIds: [entry.candidate.id],
730
+ }
731
+ : {
732
+ id: `boundary-${entry.target.id}`,
733
+ timestampSeconds: entry.target.idealSeconds,
734
+ kind: "duration_fallback",
735
+ confidence: 0,
736
+ evidenceIds: [],
737
+ });
738
+ }
739
+ }
740
+ return output;
741
+ }
742
+ function preliminaryShots(boundaries) {
743
+ return boundaries.slice(0, -1).map((boundary, index) => {
744
+ const next = boundaries[index + 1];
745
+ return {
746
+ id: `shot-${String(index + 1).padStart(3, "0")}`,
747
+ startSeconds: boundary.timestampSeconds,
748
+ endSeconds: next.timestampSeconds,
749
+ startBoundaryKind: boundary.kind,
750
+ endBoundaryKind: next.kind,
751
+ };
752
+ });
753
+ }
754
+ function shotLabelContent(prompt, duration, shots, boundaries, frames, transcript) {
755
+ const content = [{
756
+ type: "input_text",
757
+ text: [
758
+ `Analysis brief:\n${prompt}`,
759
+ `Source duration: ${duration} seconds`,
760
+ `Immutable boundaries:\n${JSON.stringify(boundaries)}`,
761
+ `Shots to label:\n${JSON.stringify(shots)}`,
762
+ transcript
763
+ ? `Transcript:\n${transcript.slice(0, 30_000)}`
764
+ : "Transcript: unavailable",
765
+ ].join("\n\n"),
766
+ }];
767
+ for (const frame of frames) {
768
+ content.push({
769
+ type: "input_text",
770
+ text: `Frame timestamp: ${frame.timestampSeconds.toFixed(3)} seconds`,
771
+ });
772
+ content.push({
773
+ type: "input_image",
774
+ image_url: frame.dataUrl,
775
+ detail: "high",
776
+ });
777
+ }
778
+ return content;
779
+ }
780
+ function llmExchangeStage(request) {
781
+ const format = request.text?.format;
782
+ return format?.type === "json_schema"
783
+ ? format.name
784
+ : "openai_responses";
785
+ }
786
+ async function labelShots(responses, prompt, duration, shots, boundaries, frames, transcript, signal) {
787
+ const response = await responses(intelligenceRequest({
788
+ instructions: [
789
+ "Return one concise evidence-grounded label and visual prompt for every supplied shotId.",
790
+ "Do not add, remove, merge, split, or rename shot IDs.",
791
+ "Do not invent people, objects, dialogue, or events.",
792
+ ].join("\n"),
793
+ input: [{
794
+ role: "user",
795
+ content: shotLabelContent(prompt, duration, shots, boundaries, frames, transcript),
796
+ }],
797
+ text: {
798
+ format: {
799
+ type: "json_schema",
800
+ name: "notebook_shot_labels",
801
+ schema: SHOT_LABEL_JSON_SCHEMA,
802
+ strict: true,
803
+ },
804
+ },
805
+ max_output_tokens: 4096,
806
+ }, { reasoningEffort: ANALYSIS_REASONING_EFFORT }), signal);
807
+ const parsed = ShotLabelSchema.parse(JSON.parse(firstResponseText(response)));
808
+ const labels = new Map(parsed.shots.map((shot) => [shot.shotId, shot]));
809
+ return {
810
+ shots: shots.map((shot, index) => {
811
+ const label = labels.get(shot.id);
812
+ return {
813
+ startSeconds: roundTime(shot.startSeconds),
814
+ endSeconds: roundTime(shot.endSeconds),
815
+ label: label?.label.trim() || `Shot ${index + 1}`,
816
+ prompt: label?.prompt.trim()
817
+ || `Visual continuity from ${shot.startSeconds.toFixed(3)} to ${shot.endSeconds.toFixed(3)} seconds.`,
818
+ };
819
+ }),
820
+ };
821
+ }
822
+ export async function analyzeNotebookSource(engine, sourceArtifactId, prompt, options = {}) {
823
+ const duration = await readDuration(engine, sourceArtifactId, options.sourceDurationSeconds);
824
+ const frames = await readFrames(engine, sourceArtifactId);
825
+ const transcript = await transcribeMissingText(engine, sourceArtifactId, await readTranscript(engine, sourceArtifactId), options.signal);
826
+ const waveformResult = await engine.metadata.waveforms.read(sourceArtifactId);
827
+ const waveform = waveformResult.ok ? waveformResult.value.peaks : [];
828
+ const audioCandidates = buildAudioCandidates(transcript.timedTranscript, waveform, duration);
829
+ const visualEvidence = options.visualEvidence
830
+ ? suppliedVisualEvidence(options.visualEvidence)
831
+ : await collectVisualEvidence(engine, sourceArtifactId, duration, options.signal);
832
+ const providerResponses = options.responses
833
+ ?? buildIntelligenceResponses(getApiKey("OPENAI_API_KEY") ?? undefined);
834
+ const llmExchanges = [];
835
+ const responses = async (request, signal) => {
836
+ const exchangeIndex = llmExchanges.length;
837
+ llmExchanges.push(null);
838
+ const response = await providerResponses(request, signal);
839
+ llmExchanges[exchangeIndex] = {
840
+ provider: "openai",
841
+ model: request.model,
842
+ stage: llmExchangeStage(request),
843
+ input: request,
844
+ response,
845
+ };
846
+ return response;
847
+ };
848
+ const visualDecisions = await validateCameraCandidates(responses, visualEvidence, options.signal);
849
+ const visualById = new Map(visualEvidence.map((entry) => [
850
+ entry.candidate.id,
851
+ entry.candidate,
852
+ ]));
853
+ const cameraBoundaries = visualDecisions
854
+ .filter((decision) => decision.accepted)
855
+ .flatMap((decision) => {
856
+ const candidate = visualById.get(decision.candidateId);
857
+ return candidate
858
+ ? [{
859
+ id: `boundary-${candidate.id}`,
860
+ timestampSeconds: candidate.timestampSeconds,
861
+ kind: "camera_cut",
862
+ confidence: decision.confidence,
863
+ evidenceIds: [candidate.id],
864
+ }]
865
+ : [];
866
+ })
867
+ .sort((left, right) => left.timestampSeconds - right.timestampSeconds);
868
+ const targets = buildBoundaryTargets(cameraBoundaries, audioCandidates, duration);
869
+ const audioSelections = await selectAudioCandidates(responses, targets, audioCandidates, options.signal);
870
+ const audioBoundaries = selectedAudioBoundaries(targets, audioSelections, audioCandidates, cameraBoundaries, duration);
871
+ const sourceStartBoundary = {
872
+ id: "boundary-source-start",
873
+ timestampSeconds: 0,
874
+ kind: "source_start",
875
+ confidence: 1,
876
+ evidenceIds: [],
877
+ };
878
+ const sourceEndBoundary = {
879
+ id: "boundary-source-end",
880
+ timestampSeconds: roundTime(duration),
881
+ kind: "source_end",
882
+ confidence: 1,
883
+ evidenceIds: [],
884
+ };
885
+ const boundaries = [
886
+ sourceStartBoundary,
887
+ ...cameraBoundaries,
888
+ ...audioBoundaries,
889
+ sourceEndBoundary,
890
+ ].sort((left, right) => left.timestampSeconds - right.timestampSeconds);
891
+ const shots = preliminaryShots(boundaries);
892
+ const rawOutput = await labelShots(responses, prompt, duration, shots, boundaries, frames, transcript.text, options.signal);
893
+ const selectedAudioCandidateIds = audioBoundaries
894
+ .flatMap((boundary) => boundary.kind === "audio_transition" ? boundary.evidenceIds : []);
895
+ const evidence = {
896
+ visualCandidates: visualEvidence.map((entry) => entry.candidate),
897
+ visualDecisions,
898
+ audioCandidates,
899
+ selectedAudioCandidateIds,
900
+ transcript: {
901
+ included: transcript.text !== null,
902
+ timed: transcript.timedTranscript !== null,
903
+ ...(transcript.provider ? { provider: transcript.provider } : {}),
904
+ ...(transcript.model ? { model: transcript.model } : {}),
905
+ ...(transcript.transcriptId
906
+ ? { transcriptId: transcript.transcriptId }
907
+ : {}),
908
+ },
909
+ };
910
+ return {
911
+ rawOutput,
912
+ sourceDurationSeconds: duration,
913
+ llmExchanges: llmExchanges.filter((exchange) => exchange !== null),
914
+ evidence,
915
+ boundaries,
916
+ request: {
917
+ provider: "openai",
918
+ model: INTELLIGENCE_MODEL,
919
+ reasoning: { effort: ANALYSIS_REASONING_EFFORT },
920
+ mode: "live",
921
+ pipelineVersion: 3,
922
+ prompt,
923
+ sourceArtifactId,
924
+ sourceDurationSeconds: duration,
925
+ stages: [
926
+ "visual_candidate_detection",
927
+ "camera_validation",
928
+ "audio_boundary_selection",
929
+ "shot_labeling",
930
+ ],
931
+ frameTimestamps: frames.map((frame) => frame.timestampSeconds),
932
+ visualCandidateCount: visualEvidence.length,
933
+ acceptedCameraCutCount: cameraBoundaries.length,
934
+ audioCandidateCount: audioCandidates.length,
935
+ selectedAudioBoundaryCount: selectedAudioCandidateIds.length,
936
+ durationFallbackCount: audioBoundaries.filter((boundary) => boundary.kind === "duration_fallback").length,
937
+ transcriptIncluded: transcript.text !== null,
938
+ timedTranscriptIncluded: transcript.timedTranscript !== null,
939
+ },
940
+ };
941
+ }