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,169 @@
1
+ import { createReadStream, createWriteStream } from "node:fs";
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { pipeline } from "node:stream/promises";
4
+ import { dirname, join, resolve, sep } from "node:path";
5
+ import { getEngineRoot } from "./queue.js";
6
+ import { archiveKey, clipPrefix, createArchiveB2, getArchiveConfig, getJson, getText, listObjects, missingArchiveConfigKeys, objectExists, putJson, putText, uploadFileToB2, } from "./archive-b2.js";
7
+ /**
8
+ * The global archive is outside every forkable book catalog. Its indexes and
9
+ * marker objects are derived from immutable media, transcript, and ingest
10
+ * objects; book-owned archive links remain canonical artifact metadata.
11
+ */
12
+ export const ARCHIVE_STORE_BOUNDARY = {
13
+ role: "rebuildable-app-cache",
14
+ bookAuthority: "artifact-metadata",
15
+ derivedObjects: ["index", "topic-marker", "person-marker"],
16
+ };
17
+ export function localArchiveDir() {
18
+ const fromEnv = process.env.VIDEOBOOK_ARCHIVE_DIR;
19
+ if (fromEnv)
20
+ return resolve(fromEnv);
21
+ return join(resolve(getEngineRoot()), "archive");
22
+ }
23
+ function isArchiveB2Configured() {
24
+ return missingArchiveConfigKeys().length === 0;
25
+ }
26
+ export function getArchiveStore() {
27
+ if (isArchiveB2Configured())
28
+ return createB2ArchiveStore();
29
+ return createLocalArchiveStore(localArchiveDir());
30
+ }
31
+ function createB2ArchiveStore(config = getArchiveConfig()) {
32
+ const b2 = createArchiveB2(config);
33
+ return {
34
+ kind: "b2",
35
+ prefix: config.prefix,
36
+ exists: (key) => objectExists(b2, config.bucket, key),
37
+ putJson: (key, value) => putJson(b2, config.bucket, key, value),
38
+ putText: (key, value) => putText(b2, config.bucket, key, value),
39
+ getJson: (key) => getJson(b2, config.bucket, key),
40
+ getText: (key) => getText(b2, config.bucket, key),
41
+ getBytes: async (key) => {
42
+ try {
43
+ const response = await b2.objects.get({ bucket: config.bucket, key });
44
+ return new Uint8Array(response.body);
45
+ }
46
+ catch (error) {
47
+ if (error?.status === 404)
48
+ return null;
49
+ throw error;
50
+ }
51
+ },
52
+ list: (prefix) => listObjects(b2, config.bucket, prefix),
53
+ uploadFile: (key, filePath, contentType) => uploadFileToB2(b2, config.bucket, key, filePath, contentType),
54
+ presignGet: (key, expiresInSeconds) => {
55
+ const signed = b2.presign.getObject({ bucket: config.bucket, key, expiresIn: expiresInSeconds });
56
+ return signed?.url ? { url: signed.url, expiresAt: signed.expiresAt ?? null } : null;
57
+ },
58
+ };
59
+ }
60
+ function localPathForKey(rootDir, key) {
61
+ const segments = key.split("/").filter(Boolean);
62
+ if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
63
+ throw new Error(`Invalid archive key: ${key}`);
64
+ }
65
+ const fullPath = join(rootDir, ...segments);
66
+ if (fullPath !== rootDir && !fullPath.startsWith(rootDir + sep)) {
67
+ throw new Error(`Invalid archive key: ${key}`);
68
+ }
69
+ return fullPath;
70
+ }
71
+ function isFsNotFound(error) {
72
+ return error?.code === "ENOENT";
73
+ }
74
+ async function readLocalFile(rootDir, key) {
75
+ try {
76
+ return await readFile(localPathForKey(rootDir, key));
77
+ }
78
+ catch (error) {
79
+ if (isFsNotFound(error))
80
+ return null;
81
+ throw error;
82
+ }
83
+ }
84
+ async function writeLocalFile(rootDir, key, body) {
85
+ const fullPath = localPathForKey(rootDir, key);
86
+ await mkdir(dirname(fullPath), { recursive: true });
87
+ await writeFile(fullPath, body);
88
+ }
89
+ async function walkLocalKeys(rootDir, dir, out) {
90
+ let entries;
91
+ try {
92
+ entries = await readdir(dir, { withFileTypes: true });
93
+ }
94
+ catch (error) {
95
+ if (isFsNotFound(error))
96
+ return;
97
+ throw error;
98
+ }
99
+ for (const entry of entries) {
100
+ const fullPath = join(dir, entry.name);
101
+ if (entry.isDirectory()) {
102
+ await walkLocalKeys(rootDir, fullPath, out);
103
+ continue;
104
+ }
105
+ if (!entry.isFile())
106
+ continue;
107
+ const fileStat = await stat(fullPath);
108
+ out.push({
109
+ key: fullPath.slice(rootDir.length + 1).split(sep).join("/"),
110
+ size: fileStat.size,
111
+ lastModified: fileStat.mtime.toISOString(),
112
+ });
113
+ }
114
+ }
115
+ export function localArchiveMediaUrl(key) {
116
+ return `/archive-files/${key.split("/").map(encodeURIComponent).join("/")}`;
117
+ }
118
+ export function createLocalArchiveStore(rootDir) {
119
+ const root = resolve(rootDir);
120
+ return {
121
+ kind: "local",
122
+ prefix: "",
123
+ exists: async (key) => {
124
+ try {
125
+ const fileStat = await stat(localPathForKey(root, key));
126
+ return fileStat.isFile();
127
+ }
128
+ catch (error) {
129
+ if (isFsNotFound(error))
130
+ return false;
131
+ throw error;
132
+ }
133
+ },
134
+ putJson: (key, value) => writeLocalFile(root, key, JSON.stringify(value, null, 2)),
135
+ putText: (key, value) => writeLocalFile(root, key, value),
136
+ getJson: async (key) => {
137
+ const body = await readLocalFile(root, key);
138
+ return body === null ? null : JSON.parse(body.toString("utf8"));
139
+ },
140
+ getText: async (key) => {
141
+ const body = await readLocalFile(root, key);
142
+ return body === null ? null : body.toString("utf8");
143
+ },
144
+ getBytes: async (key) => {
145
+ const body = await readLocalFile(root, key);
146
+ return body === null ? null : new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength));
147
+ },
148
+ list: async (prefix) => {
149
+ const out = [];
150
+ await walkLocalKeys(root, root, out);
151
+ return out
152
+ .filter((item) => item.key.startsWith(prefix))
153
+ .sort((a, b) => a.key.localeCompare(b.key));
154
+ },
155
+ uploadFile: async (key, filePath) => {
156
+ const fullPath = localPathForKey(root, key);
157
+ await mkdir(dirname(fullPath), { recursive: true });
158
+ await pipeline(createReadStream(filePath), createWriteStream(fullPath));
159
+ },
160
+ presignGet: (key) => ({ url: localArchiveMediaUrl(key), expiresAt: null }),
161
+ };
162
+ }
163
+ export async function readArchiveIngests(store, sha256) {
164
+ const keys = (await store.list(archiveKey(store, clipPrefix(sha256))))
165
+ .map((item) => item.key)
166
+ .filter((key) => /\/ingest-[^/]+\.json$/.test(key));
167
+ const ingests = await Promise.all(keys.map((key) => store.getJson(key)));
168
+ return ingests.filter((ingest) => ingest !== null);
169
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Display labels for generated artifacts.
3
+ *
4
+ * Labels are free text: never parsed, never matched, never a lookup key.
5
+ * They exist so a person scanning the library can tell one render from
6
+ * another, so they read as prose rather than as the hyphenated identifiers
7
+ * the old slug scheme required.
8
+ */
9
+ const MAX_SUMMARY_CHARS = 60;
10
+ /** Collapse a prompt to a short, readable fragment. */
11
+ export function promptSummary(prompt, maxChars = MAX_SUMMARY_CHARS) {
12
+ const text = (prompt ?? "").replace(/\s+/gu, " ").trim();
13
+ if (text.length <= maxChars)
14
+ return text;
15
+ const clipped = text.slice(0, maxChars);
16
+ const lastSpace = clipped.lastIndexOf(" ");
17
+ return `${(lastSpace > maxChars / 2 ? clipped.slice(0, lastSpace) : clipped).trimEnd()}…`;
18
+ }
19
+ /**
20
+ * Label for a generation output, e.g.
21
+ * `kling-3.0/video — a cat dancing in the rain`.
22
+ * Falls back to the model alone when there is no prompt to summarize.
23
+ */
24
+ export function generationLabel(model, prompt) {
25
+ const summary = promptSummary(prompt);
26
+ return summary ? `${model} — ${summary}` : model;
27
+ }
28
+ /**
29
+ * Label for an artifact derived from another one, e.g.
30
+ * `upscaled — beach sunset`. `source` is the upstream artifact's label,
31
+ * falling back to its id when it has none.
32
+ */
33
+ export function derivedLabel(operation, source) {
34
+ const stem = (source ?? "").trim();
35
+ return stem ? `${operation} — ${stem}` : operation;
36
+ }
@@ -0,0 +1,49 @@
1
+ import { findPrimaryMediaFile } from "videobook-engine";
2
+ // Post-resolution prompts and tool args reference media as @<artifact-id>
3
+ // tokens (grid addresses are resolved to artifact ids before any tool or
4
+ // payload sees them).
5
+ const ARTIFACT_ID_TAG_PATTERN = /@([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?![\w-])/giu;
6
+ // `@s01`-`@s99` address the primary sequence's slots by ordinal; they expand
7
+ // to the artifact id occupying that slot before any other resolution runs.
8
+ const SLOT_TAG_PATTERN = /@s(\d{1,2})(?![\w-])/giu;
9
+ export function expandSlotRefs(text, slotArtifactIds) {
10
+ return text.replace(SLOT_TAG_PATTERN, (match, ordinal) => {
11
+ const artifactId = slotArtifactIds[Number(ordinal) - 1];
12
+ return artifactId ? `@${artifactId}` : match;
13
+ });
14
+ }
15
+ /** Bare artifact ids mentioned in the text, in first-mention order. */
16
+ export function parseArtifactIdTags(text) {
17
+ const tags = [];
18
+ for (const match of text.matchAll(ARTIFACT_ID_TAG_PATTERN)) {
19
+ const id = (match[1] ?? "").toLowerCase();
20
+ if (id && !tags.includes(id))
21
+ tags.push(id);
22
+ }
23
+ return tags;
24
+ }
25
+ export async function resolveArtifactRefs(engine, references) {
26
+ const resolved = [];
27
+ const unresolved = [];
28
+ for (const reference of references) {
29
+ const artifactId = reference.replace(/^@/u, "").toLowerCase();
30
+ const artifact = engine.artifacts.get(artifactId);
31
+ if (!artifact.ok) {
32
+ unresolved.push(`@${artifactId}`);
33
+ continue;
34
+ }
35
+ const manifest = await engine.files.manifest(artifactId);
36
+ const primary = manifest.ok
37
+ ? findPrimaryMediaFile(manifest.value.files, artifact.value.kind)
38
+ : null;
39
+ resolved.push({
40
+ tag: `@${artifactId}`,
41
+ artifactId,
42
+ ...(artifact.value.label ? { artifactLabel: artifact.value.label } : {}),
43
+ artifactType: artifact.value.kind,
44
+ filePath: primary ? primary.name : null,
45
+ workspacePath: artifact.value.path,
46
+ });
47
+ }
48
+ return { resolved, unresolved };
49
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Turning analysis a book already paid for into editorial tags.
3
+ *
4
+ * Nothing here calls a provider. Image analysis, video analysis and
5
+ * transcripts are produced elsewhere and cached on the artifact; this module
6
+ * reads that evidence and decides what it actually supports. Two rules shape
7
+ * every extractor:
8
+ *
9
+ * 1. An unsupported claim is omitted, not softened. Evenly spaced frames can
10
+ * show a setting and a framing; they cannot establish that the camera
11
+ * tracked, so no extractor offers movement from them.
12
+ * 2. Absence of evidence is a state, not an empty tag set to hide. Callers get
13
+ * an outcome they can report, so "we found nothing useful" never looks the
14
+ * same as "we never looked".
15
+ */
16
+ import { canonicalizeTag, isFrameTag, matchVocabularyTerms, selectEditorialTags, } from "./tag-vocabulary.js";
17
+ function record(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value)
19
+ ? value
20
+ : null;
21
+ }
22
+ function strings(value) {
23
+ if (typeof value === "string")
24
+ return value.trim() ? [value] : [];
25
+ if (!Array.isArray(value))
26
+ return [];
27
+ return value.flatMap(strings);
28
+ }
29
+ function result(tags) {
30
+ const selected = selectEditorialTags(tags);
31
+ return {
32
+ tags: selected,
33
+ outcome: selected.length > 0 ? "tagged" : "no_useful_evidence",
34
+ };
35
+ }
36
+ function structuredCandidates(value) {
37
+ if (!Array.isArray(value))
38
+ return [];
39
+ return value.flatMap((entry) => {
40
+ const row = record(entry);
41
+ const facet = typeof row?.facet === "string" ? row.facet : null;
42
+ const label = typeof row?.label === "string" ? row.label : null;
43
+ if (!facet || !label)
44
+ return [];
45
+ const canonical = canonicalizeTag(facet, label, {
46
+ allowUnlisted: true,
47
+ });
48
+ return canonical ? [canonical] : [];
49
+ });
50
+ }
51
+ /**
52
+ * Images: the describe_image response carries structured tags alongside the
53
+ * reproduction prompt, so tagging costs no extra request. A cached analysis
54
+ * written before tags existed simply yields nothing — the shot/pose fields
55
+ * are prompt vocabulary, not grouping terms, and mining them would invent
56
+ * claims the model never made.
57
+ */
58
+ export function tagsFromImageAnalysis(analysis) {
59
+ const document = record(analysis);
60
+ if (!document || !Array.isArray(document.tags)
61
+ || (document.tags.length > 0 && !document.tags.some((entry) => {
62
+ const tag = record(entry);
63
+ return typeof tag?.facet === "string" && ["people", "places", "editing"].includes(tag.facet)
64
+ && typeof tag.label === "string";
65
+ })))
66
+ return { tags: [], outcome: "no_evidence" };
67
+ return result(structuredCandidates(document.tags).filter(isFrameTag));
68
+ }
69
+ /**
70
+ * Video and final renders: analyze_video's cached document. Its `tags`,
71
+ * `setting`, `subjects` and scene tags are free text written for search, so
72
+ * every term goes through the vocabulary and most are discarded. A fallback
73
+ * document — the empty shape the analyzer writes when the provider failed —
74
+ * is not evidence at all.
75
+ */
76
+ export function tagsFromVideoAnalysis(analysis) {
77
+ const document = record(analysis);
78
+ if (!document)
79
+ return { tags: [], outcome: "no_evidence" };
80
+ if (isFallbackAnalysis(document))
81
+ return { tags: [], outcome: "no_evidence" };
82
+ if (Array.isArray(document.editorialTags)) {
83
+ return result(structuredCandidates(document.editorialTags).filter(isFrameTag));
84
+ }
85
+ const scenes = Array.isArray(document.scenes) ? document.scenes : [];
86
+ const sceneTags = scenes.flatMap((scene) => strings(record(scene)?.tags));
87
+ // Setting first: where something happens is the most reliable grouping
88
+ // signal a frame sample supports, then who is in it, then the analyzer's
89
+ // own top-level terms, then per-scene detail.
90
+ const settings = strings(document.setting);
91
+ const freeText = [
92
+ ...strings(document.tags),
93
+ ...sceneTags,
94
+ ];
95
+ const candidates = [
96
+ // The setting field holds a place and nothing else, so a concrete term it
97
+ // names is kept even when the vocabulary has never seen it.
98
+ ...settings.flatMap((phrase) => {
99
+ const listed = matchVocabularyTerms("places", phrase);
100
+ if (listed.length > 0)
101
+ return listed;
102
+ const unlisted = canonicalizeTag("places", phrase, { allowUnlisted: true });
103
+ return unlisted ? [unlisted] : [];
104
+ }),
105
+ ...freeText.flatMap((phrase) => ["places", "people", "editing"].flatMap((facet) => {
106
+ const exact = canonicalizeTag(facet, phrase);
107
+ return exact ? [exact] : [];
108
+ })),
109
+ ...strings(document.subjects).flatMap((phrase) => {
110
+ const exact = canonicalizeTag("people", phrase);
111
+ return exact ? [exact] : [];
112
+ }),
113
+ ];
114
+ return result(candidates.filter(isFrameTag));
115
+ }
116
+ /**
117
+ * An analyze_video run that failed writes a document with every field empty.
118
+ * Blessing that as "analyzed, nothing found" would fence the artifact against
119
+ * a later successful run, so it counts as no evidence.
120
+ */
121
+ export function isFallbackAnalysis(document) {
122
+ const summary = typeof document.summary === "string" ? document.summary.trim() : "";
123
+ const scenes = Array.isArray(document.scenes) ? document.scenes.length : 0;
124
+ const tags = Array.isArray(document.tags) ? document.tags.length : 0;
125
+ const subjects = Array.isArray(document.subjects) ? document.subjects.length : 0;
126
+ const setting = typeof document.setting === "string" ? document.setting.trim() : "";
127
+ return summary === "" && scenes === 0 && tags === 0 && subjects === 0 && setting === "";
128
+ }
129
+ /**
130
+ * Audio: what a transcript can honestly support is whether the recording
131
+ * carries speech and whether more than one person speaks. Topic and genre are
132
+ * not editing decisions, and a name in a transcript identifies nobody on
133
+ * screen, so neither becomes a tag.
134
+ */
135
+ export function tagsFromTranscript(evidence) {
136
+ if (!evidence)
137
+ return { tags: [], outcome: "no_evidence" };
138
+ const candidates = [];
139
+ if (evidence.wordCount > 0)
140
+ candidates.push({ facet: "editing", label: "dialogue" });
141
+ return result(candidates);
142
+ }
143
+ /**
144
+ * Text-bearing artifacts. A script's sluglines are the one place stored text
145
+ * genuinely encodes place and time — "INT. KITCHEN - NIGHT" is a structured
146
+ * statement, not a keyword that happens to appear. Everything else in a
147
+ * document is dialogue and direction, which say nothing about how the footage
148
+ * that may one day exist was shot, so nothing else is read.
149
+ */
150
+ export function tagsFromScriptText(text) {
151
+ if (!text || !text.trim())
152
+ return { tags: [], outcome: "no_evidence" };
153
+ const candidates = [];
154
+ const sluglines = text.match(/^\s*(INT|EXT)[.\s][^\n]*/gimu) ?? [];
155
+ for (const line of sluglines) {
156
+ const upper = line.toUpperCase();
157
+ if (/^\s*INT/u.test(upper))
158
+ candidates.push({ facet: "places", label: "interior" });
159
+ if (/^\s*EXT/u.test(upper))
160
+ candidates.push({ facet: "places", label: "exterior" });
161
+ if (/[-\s]DAY\b/u.test(upper))
162
+ candidates.push({ facet: "places", label: "day" });
163
+ if (/[-\s]NIGHT\b/u.test(upper))
164
+ candidates.push({ facet: "places", label: "night" });
165
+ }
166
+ if (candidates.length === 0)
167
+ return { tags: [], outcome: "no_useful_evidence" };
168
+ return result(candidates);
169
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Asset tag projection: the one place that turns engine tag state into the
3
+ * snake_case records MCP payloads carry, and the one place that decides
4
+ * whether an asset matches a tag filter.
5
+ *
6
+ * The engine owns storage, normalization and provenance; this module owns
7
+ * presentation and the exact-match filter the Library and list_assets share,
8
+ * so a tag reads the same way in an asset payload, a search result and a
9
+ * filter argument.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { resolveTagInput } from "./tag-vocabulary.js";
13
+ import { TAG_FACETS, TAG_READ_BATCH_MAX, isTagFacet, normalizeTagIdentity, tagCanonicalKey, } from "videobook-engine";
14
+ export const EMPTY_TAG_SUMMARY = Object.freeze({
15
+ effective: [],
16
+ manual: [],
17
+ automatic: [],
18
+ dismissed: [],
19
+ analysis: null,
20
+ fingerprint: "",
21
+ });
22
+ function tagRecord(tag) {
23
+ const identity = normalizeTagIdentity(resolveTagInput(tag));
24
+ return {
25
+ facet: tag.facet,
26
+ key: identity.key,
27
+ label: identity.label,
28
+ origin: tag.origin,
29
+ entity_id: tag.entityId ?? null,
30
+ };
31
+ }
32
+ function dismissedRecord(tag) {
33
+ return normalizeTagIdentity(resolveTagInput(tag));
34
+ }
35
+ function automaticRecord(tag) {
36
+ return tagRecord(tag);
37
+ }
38
+ /**
39
+ * Identity-only digest: labels and timestamps are display detail, so a
40
+ * re-analysis that returns the same identities leaves the fingerprint alone
41
+ * and the client skips a needless asset refresh.
42
+ */
43
+ function tagFingerprint(state) {
44
+ const parts = [
45
+ ...state.effective.map((tag) => `e:${tag.facet}/${tag.key}/${tag.origin}`),
46
+ ...state.dismissed.map((tag) => `d:${tag.facet}/${tag.key}`),
47
+ ].sort();
48
+ if (parts.length === 0)
49
+ return "";
50
+ return createHash("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
51
+ }
52
+ export function assetTagSummary(state) {
53
+ const unique = (tags) => [...new Map(tags.map((tag) => [`${tag.facet}/${tag.key}`, tag])).values()];
54
+ const manual = unique(state.manual.map(tagRecord));
55
+ const automatic = unique(state.automatic.map(automaticRecord));
56
+ const dismissed = unique(state.dismissed.map(dismissedRecord));
57
+ const suppressed = new Set(dismissed.map((tag) => `${tag.facet}/${tag.key}`));
58
+ const effective = unique([
59
+ ...state.effective.filter((tag) => tag.origin === "automatic").map(tagRecord)
60
+ .filter((tag) => !suppressed.has(`${tag.facet}/${tag.key}`)),
61
+ ...manual,
62
+ ]).sort((left, right) => left.facet.localeCompare(right.facet) || left.key.localeCompare(right.key));
63
+ return {
64
+ effective,
65
+ manual,
66
+ automatic,
67
+ dismissed,
68
+ analysis: state.snapshot
69
+ ? {
70
+ source_hash: state.snapshot.sourceHash,
71
+ generator: state.snapshot.generator,
72
+ model: state.snapshot.model ?? null,
73
+ extractor_version: state.snapshot.extractorVersion,
74
+ tag_count: state.snapshot.tagCount,
75
+ generation: state.snapshot.generation,
76
+ analyzed_at: state.snapshot.analyzedAt,
77
+ stale: state.snapshot.stale,
78
+ }
79
+ : null,
80
+ fingerprint: tagFingerprint(state),
81
+ };
82
+ }
83
+ /**
84
+ * Batched tag read for a list of artifacts. Reads are chunked to the engine's
85
+ * documented batch ceiling, and an artifact the engine cannot read (a
86
+ * synthetic notebook row, say) simply has no entry.
87
+ */
88
+ export function readAssetTagSummaries(engine, artifactIds) {
89
+ const summaries = new Map();
90
+ for (let start = 0; start < artifactIds.length; start += TAG_READ_BATCH_MAX) {
91
+ const batch = artifactIds.slice(start, start + TAG_READ_BATCH_MAX);
92
+ const result = engine.tags.readMany(batch);
93
+ if (!result.ok)
94
+ continue;
95
+ for (const state of result.value) {
96
+ summaries.set(state.artifactId, assetTagSummary(state));
97
+ }
98
+ }
99
+ return summaries;
100
+ }
101
+ /**
102
+ * Parses one filter term. A bare label matches the identity in any facet,
103
+ * which is what a user typing "kitchen" into a filter box means; a
104
+ * facet-qualified term keeps `people/handheld` and `editing/handheld` apart.
105
+ */
106
+ function normalizeSelector(selector) {
107
+ if (selector.facet !== undefined) {
108
+ if (!isTagFacet(selector.facet)) {
109
+ throw new Error(`Unknown tag facet "${selector.facet}"; expected one of ${TAG_FACETS.join(", ")}`);
110
+ }
111
+ const identity = normalizeTagIdentity(resolveTagInput({
112
+ facet: selector.facet,
113
+ label: selector.label,
114
+ }));
115
+ return { facet: identity.facet, key: identity.key };
116
+ }
117
+ const key = tagCanonicalKey(selector.label);
118
+ if (!key)
119
+ throw new Error("Tag label is required");
120
+ return { facet: null, key };
121
+ }
122
+ function matchesSelector(tags, selector) {
123
+ return tags.some((tag) => tagCanonicalKey(resolveTagInput(tag).label) === tagCanonicalKey(resolveTagInput({
124
+ facet: tag.facet, label: selector.key,
125
+ }).label)
126
+ && (selector.facet === null || tag.facet === selector.facet));
127
+ }
128
+ /** True when no term was supplied — an absent filter matches everything. */
129
+ function isEmptyTagFilter(filter) {
130
+ return !filter
131
+ || ((filter.all?.length ?? 0) === 0 && (filter.any?.length ?? 0) === 0);
132
+ }
133
+ /**
134
+ * Exact identity matching against the effective set only: a dismissed tag
135
+ * must not match, and neither may a transcript that merely mentions the word
136
+ * — that is what semantic search is for.
137
+ */
138
+ export function matchesTagFilter(summary, filter) {
139
+ if (isEmptyTagFilter(filter))
140
+ return true;
141
+ const tags = summary?.effective ?? [];
142
+ const all = (filter?.all ?? []).map(normalizeSelector);
143
+ const any = (filter?.any ?? []).map(normalizeSelector);
144
+ if (!all.every((selector) => matchesSelector(tags, selector)))
145
+ return false;
146
+ if (any.length > 0 && !any.some((selector) => matchesSelector(tags, selector))) {
147
+ return false;
148
+ }
149
+ return true;
150
+ }
151
+ /**
152
+ * Facet counts over an already-filtered result set, so a caller can offer the
153
+ * groups that actually narrow what it is looking at rather than every tag in
154
+ * the book.
155
+ */
156
+ export function tagFacetCounts(assets, limit = 50) {
157
+ const counts = new Map();
158
+ for (const asset of assets) {
159
+ const seen = new Set();
160
+ for (const tag of asset.tags?.effective ?? []) {
161
+ const identity = `${tag.facet}/${tag.key}`;
162
+ if (seen.has(identity))
163
+ continue;
164
+ seen.add(identity);
165
+ const existing = counts.get(identity);
166
+ if (existing) {
167
+ existing.assets += 1;
168
+ continue;
169
+ }
170
+ counts.set(identity, {
171
+ facet: tag.facet,
172
+ key: tag.key,
173
+ label: tag.label,
174
+ assets: 1,
175
+ });
176
+ }
177
+ }
178
+ return [...counts.values()]
179
+ .sort((left, right) => right.assets - left.assets
180
+ || left.facet.localeCompare(right.facet)
181
+ || left.key.localeCompare(right.key))
182
+ .slice(0, limit);
183
+ }
184
+ export const EMPTY_TRANSFER_SUMMARY = {
185
+ imported_manual: 0,
186
+ imported_dismissals: 0,
187
+ imported_automatic: 0,
188
+ skipped_automatic: false,
189
+ };
190
+ /**
191
+ * Moves one artifact's tag state onto a copy, in either the same engine or
192
+ * another book's. The engine re-normalizes every label into destination-local
193
+ * identities and validates automatic evidence against the destination's own
194
+ * files, so this never hand-copies rows. Copying nothing is not a write: an
195
+ * untagged source leaves the destination catalog untouched.
196
+ */
197
+ export async function transferAssetTags(source, destination) {
198
+ const exported = source.engine.tags.export(source.artifactId);
199
+ if (!exported.ok)
200
+ throw new Error(exported.error.message);
201
+ const snapshot = exported.value;
202
+ const empty = snapshot.manual.length === 0
203
+ && snapshot.dismissed.length === 0
204
+ && snapshot.automatic === undefined;
205
+ if (empty)
206
+ return EMPTY_TRANSFER_SUMMARY;
207
+ const imported = await destination.engine.tags.import(destination.artifactId, snapshot);
208
+ if (!imported.ok)
209
+ throw new Error(imported.error.message);
210
+ return {
211
+ imported_manual: imported.value.importedManual,
212
+ imported_dismissals: imported.value.importedDismissals,
213
+ imported_automatic: imported.value.importedAutomatic,
214
+ skipped_automatic: imported.value.skippedAutomatic,
215
+ };
216
+ }
217
+ /**
218
+ * The effective tag labels as one indexable string. Search indexing consumes
219
+ * this rather than the raw analysis sidecar, so a dismissed tag stays out of
220
+ * the lexical index even while the description that mentioned it remains
221
+ * searchable as evidence.
222
+ */
223
+ export function effectiveTagText(engine, artifactId) {
224
+ const state = engine.tags.read(artifactId);
225
+ if (!state.ok)
226
+ return "";
227
+ return state.value.effective.map((tag) => tag.label).join(" ");
228
+ }
@@ -0,0 +1,23 @@
1
+ const SETTINGS_KEY = "asset-last-used";
2
+ const MAX_ENTRIES = 500;
3
+ function usageEntries(stored) {
4
+ if (!stored || typeof stored !== "object" || Array.isArray(stored))
5
+ return [];
6
+ return Object.entries(stored).filter((entry) => typeof entry[1] === "number" && Number.isFinite(entry[1]));
7
+ }
8
+ export function readAssetUsage(engine) {
9
+ return new Map(usageEntries(engine.settings.get(SETTINGS_KEY)));
10
+ }
11
+ export function recordAssetUse(engine, artifactId, usedAt = Date.now()) {
12
+ const entries = usageEntries(engine.settings.get(SETTINGS_KEY))
13
+ .filter(([id]) => id !== artifactId);
14
+ const next = [[artifactId, usedAt], ...entries]
15
+ .sort((left, right) => right[1] - left[1])
16
+ .slice(0, MAX_ENTRIES);
17
+ return engine.settings.set(SETTINGS_KEY, Object.fromEntries(next)).ok;
18
+ }
19
+ export function assetRecency(createdAt, lastUsedAt) {
20
+ const created = Date.parse(createdAt);
21
+ const createdMs = Number.isFinite(created) ? created : 0;
22
+ return Math.max(createdMs, lastUsedAt ?? 0);
23
+ }