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,1452 @@
1
+ import { createKie, createXai, createAlibaba, createFal } from "../usage-providers.js";
2
+ import { describeFalError } from "../jobs/_fal-shared.js";
3
+ import { currentBookContext, withBookContext } from "../book-context.js";
4
+ import { settleFinishedUsage } from "../provider-usage.js";
5
+ import { legacyMediaForTaskType } from "../media/legacy-task-types.js";
6
+ import { parseModelId } from "../media/model-id.js";
7
+ import { computeArtifactStatus, QUEUED_TASK_ID } from "videobook-engine";
8
+ import { z } from "zod";
9
+ import { createHash } from "node:crypto";
10
+ import { join } from "node:path";
11
+ import { KieError } from "@apicity/kie";
12
+ import { XaiError } from "@apicity/xai";
13
+ import { AlibabaError } from "@apicity/alibaba";
14
+ import { FalError } from "@apicity/fal";
15
+ import { EMPTY_TAG_SUMMARY, readAssetTagSummaries, } from "../asset-tags.js";
16
+ import { logger } from "../logger.js";
17
+ import { queue } from "../queue.js";
18
+ import { getApiKey } from "../api-key.js";
19
+ import { findVideoFile, findAudioFile, findPrimaryMediaFile, RENDER_ORIENTATIONS as MEDIA_RENDER_ORIENTATIONS, } from "videobook-engine";
20
+ import { readAndAssessDialog } from "../dialog-quality.js";
21
+ import { hasSpokenAudio, PLAIN_TRANSCRIPT_FILE, readAssetTranscriptSources, TIMED_TRANSCRIPT_FILE, } from "../transcript-projection.js";
22
+ import { isAnalyzableVideo, readVideoAnalysisSummary, VIDEO_ANALYSIS_FILE, } from "../video-analysis-projection.js";
23
+ import { failPendingTaskAndLog } from "../jobs/task-failure.js";
24
+ import { findUnprocessedUploads, processExternalUpload } from "../jobs/process-upload.js";
25
+ import { planVideoTranscriptJobs } from "../jobs/process-video.js";
26
+ import { probeVideo } from "../ffmpeg.js";
27
+ import { providerEndpoints } from "../media/endpoints.js";
28
+ import { submitQueuedKieTask } from "../jobs/kie-paygate.js";
29
+ import { DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT, } from "../remotion/keyword-graphics.js";
30
+ import { invalidVideoProxyMetadata, probeStoredVideoFile, videoProxyAssessmentMatches, videoProxyMetadata, videoProxyMetadataMatches, videoProxyValidationError, } from "../video-proxy.js";
31
+ /** Upper bound on how long a pending task may sit without reaching success/fail
32
+ * before reconciliation ages it out. Guards against providers leaving jobs in an
33
+ * intermediate "waiting" state indefinitely. */
34
+ const PENDING_TASK_TIMEOUT_MS = 30 * 60 * 1000;
35
+ function analysisInfoFromMetadata(metadata) {
36
+ const value = metadata?.analysisInfo;
37
+ if (!value || typeof value !== "object" || Array.isArray(value))
38
+ return null;
39
+ const record = value;
40
+ const sourceDurationSeconds = record.sourceDurationSeconds;
41
+ if (typeof sourceDurationSeconds !== "number"
42
+ || !Number.isFinite(sourceDurationSeconds)
43
+ || !Array.isArray(record.shots)) {
44
+ return null;
45
+ }
46
+ const shots = record.shots.flatMap((value) => {
47
+ if (!value || typeof value !== "object" || Array.isArray(value))
48
+ return [];
49
+ const shot = value;
50
+ if (typeof shot.ordinal !== "number"
51
+ || typeof shot.label !== "string"
52
+ || typeof shot.startSeconds !== "number"
53
+ || typeof shot.endSeconds !== "number"
54
+ || typeof shot.durationSeconds !== "number") {
55
+ return [];
56
+ }
57
+ return [{
58
+ ordinal: shot.ordinal,
59
+ label: shot.label,
60
+ start_seconds: shot.startSeconds,
61
+ end_seconds: shot.endSeconds,
62
+ duration_seconds: shot.durationSeconds,
63
+ }];
64
+ });
65
+ return {
66
+ source_duration_seconds: sourceDurationSeconds,
67
+ output_json: null,
68
+ output_url: null,
69
+ llm_exchanges: [],
70
+ shots,
71
+ };
72
+ }
73
+ async function analysisOutputProjection(engine, artifactId) {
74
+ const read = await engine.files.read(artifactId, "analysis.json");
75
+ if (!read.ok)
76
+ return null;
77
+ try {
78
+ const outputJson = JSON.parse(read.value.toString());
79
+ if (typeof outputJson !== "object"
80
+ || outputJson === null
81
+ || Array.isArray(outputJson)) {
82
+ return { outputJson, llmExchanges: [] };
83
+ }
84
+ const output = outputJson;
85
+ const exchanges = Array.isArray(output.llmExchanges)
86
+ ? output.llmExchanges.flatMap((value) => {
87
+ if (typeof value !== "object"
88
+ || value === null
89
+ || Array.isArray(value)) {
90
+ return [];
91
+ }
92
+ const exchange = value;
93
+ const input = typeof exchange.input === "object"
94
+ && exchange.input !== null
95
+ && !Array.isArray(exchange.input)
96
+ ? exchange.input
97
+ : null;
98
+ const provider = typeof exchange.provider === "string"
99
+ ? exchange.provider
100
+ : null;
101
+ const model = typeof exchange.model === "string"
102
+ ? exchange.model
103
+ : typeof input?.model === "string"
104
+ ? input.model
105
+ : null;
106
+ const stage = typeof exchange.stage === "string"
107
+ ? exchange.stage
108
+ : null;
109
+ const text = input
110
+ && typeof input.text === "object"
111
+ && input.text !== null
112
+ && !Array.isArray(input.text)
113
+ ? input.text
114
+ : null;
115
+ const format = text
116
+ && typeof text.format === "object"
117
+ && text.format !== null
118
+ && !Array.isArray(text.format)
119
+ ? text.format
120
+ : null;
121
+ const formatType = typeof format?.type === "string"
122
+ ? format.type
123
+ : null;
124
+ const responseFormat = formatType
125
+ ? {
126
+ type: formatType,
127
+ name: typeof format?.name === "string" ? format.name : null,
128
+ strict: format?.strict === true,
129
+ }
130
+ : null;
131
+ return provider && model && stage
132
+ ? [{
133
+ provider,
134
+ model,
135
+ stage,
136
+ response_format: responseFormat,
137
+ }]
138
+ : [];
139
+ })
140
+ : [];
141
+ const compactOutput = { ...output };
142
+ delete compactOutput.llmExchanges;
143
+ return {
144
+ outputJson: compactOutput,
145
+ llmExchanges: exchanges,
146
+ };
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ function findAssetMediaFile(files, asset) {
153
+ return asset.kind === "audio"
154
+ ? findAudioFile(files, asset.kind)
155
+ : asset.kind === "video"
156
+ ? findVideoFile(files.filter((file) => file.name !== "proxy.mp4"))
157
+ : findPrimaryMediaFile(files, asset.kind);
158
+ }
159
+ async function assessStoredVideoProxy(engine, assetId, sourceFile, proxyFile, sourceProfile) {
160
+ let assessment;
161
+ try {
162
+ const proxy = await probeStoredVideoFile(engine, assetId, proxyFile.name);
163
+ const error = videoProxyValidationError(sourceProfile, proxy);
164
+ assessment = error
165
+ ? invalidVideoProxyMetadata(sourceFile, proxyFile, error)
166
+ : videoProxyMetadata(sourceFile, proxyFile, proxy);
167
+ }
168
+ catch (cause) {
169
+ const error = cause instanceof Error ? cause.message : String(cause);
170
+ assessment = invalidVideoProxyMetadata(sourceFile, proxyFile, error);
171
+ }
172
+ await engine.metadata.artifacts.write(assetId, "proxy", assessment);
173
+ return assessment.valid;
174
+ }
175
+ /** Read the OSG overlay summary from the asset's render metadata. OSG is stored
176
+ * identically across orientations, so the first orientation that carries an
177
+ * osgAssetId wins. Returns null when no overlay is configured. */
178
+ async function readOsgSummary(engine, assetId) {
179
+ for (const orient of RENDER_ORIENTATIONS) {
180
+ const meta = await engine.metadata.artifacts.read(assetId, `${orient}.render`);
181
+ if (!meta.ok)
182
+ continue;
183
+ const osgAssetId = meta.value.osgAssetId;
184
+ if (typeof osgAssetId !== "string" || !osgAssetId)
185
+ continue;
186
+ return {
187
+ assetId: osgAssetId,
188
+ position: typeof meta.value.osgPosition === "string" ? meta.value.osgPosition : "top-right",
189
+ size: typeof meta.value.osgSize === "string" ? meta.value.osgSize : "medium",
190
+ scale: typeof meta.value.osgScale === "number" ? meta.value.osgScale : 1,
191
+ opacity: typeof meta.value.osgOpacity === "number" ? meta.value.osgOpacity : 1,
192
+ startSeconds: typeof meta.value.osgStartSeconds === "number" ? meta.value.osgStartSeconds : null,
193
+ endSeconds: typeof meta.value.osgEndSeconds === "number" ? meta.value.osgEndSeconds : null,
194
+ };
195
+ }
196
+ return null;
197
+ }
198
+ function metaPrompt(meta) {
199
+ if (!meta)
200
+ return null;
201
+ const p = meta.prompt;
202
+ return typeof p === "string" ? p : null;
203
+ }
204
+ function isVeoPendingTask(task) {
205
+ return typeof task.meta?.veoModel === "string";
206
+ }
207
+ function isKiePendingTask(task) {
208
+ if (isVeoPendingTask(task))
209
+ return false;
210
+ const model = task.meta?.model;
211
+ if (typeof model === "string" && model.includes(":") && parseModelId(model)?.provider !== "kie")
212
+ return false;
213
+ return task.taskType === "media_kie" || task.taskType === "isolate_vocals"
214
+ || (legacyMediaForTaskType(task.taskType) !== null && !/^(?:xai|fal|alibaba)_/u.test(task.taskType));
215
+ }
216
+ function isQueuedKiePendingTask(task) {
217
+ return task.taskId === QUEUED_TASK_ID && isKiePendingTask(task);
218
+ }
219
+ /** failCode "500" is kie's transient internal error ("please try again
220
+ * later"). It's only worth leaving the task pending for a re-poll while the
221
+ * job is still in flight — once kie stamps a `completeTime`, the task record
222
+ * is final and re-polling the same taskId can never flip to success. Treating
223
+ * a completed 500 as transient strands the asset in "working" until the 30m
224
+ * reconciliation timeout (PENDING_TASK_TIMEOUT_MS) and buries the real failure message,
225
+ * so only a 500 with no completeTime is considered retryable. */
226
+ export function isTransientKieFailure(data) {
227
+ if (data.failCode !== "500")
228
+ return false;
229
+ return !(typeof data.completeTime === "number" && data.completeTime > 0);
230
+ }
231
+ function buildFrameDirs(directories, bookId, assetId) {
232
+ const out = {};
233
+ if (!directories)
234
+ return out;
235
+ for (const [dir, fileList] of Object.entries(directories)) {
236
+ if (!dir.endsWith("_frames"))
237
+ continue;
238
+ out[dir] = fileList
239
+ .filter((f) => f.endsWith(".jpg"))
240
+ .sort()
241
+ .map((f) => `/files/${bookId}/${assetId}/${dir}/${f}`);
242
+ }
243
+ return out;
244
+ }
245
+ const WAVEFORM_SOURCE_RE = /^original\.(mp4|mov|mp3|wav|m4a|aac|ogg|flac)$/i;
246
+ async function readWaveformOrSchedule(engine, assetId, bookId, videoFilename) {
247
+ if (!videoFilename || !WAVEFORM_SOURCE_RE.test(videoFilename)) {
248
+ return null;
249
+ }
250
+ const existing = await engine.metadata.waveforms.read(assetId);
251
+ if (existing.ok) {
252
+ // Empty peaks are a negative-cache marker ("extracted, nothing usable"):
253
+ // surface them as no-waveform so the UI is unchanged, but don't re-extract.
254
+ return existing.value.peaks.length > 0 ? existing.value.peaks : null;
255
+ }
256
+ // Waveform extraction shells out to ffmpeg and can take several seconds for
257
+ // long videos, so it must never run inline in list_assets / reconcile_project_work. Hand it to
258
+ // the job queue (deduped per asset) and return no-waveform now; the UI fills
259
+ // it in on a later poll once the background job has persisted the peaks.
260
+ scheduleWaveformExtraction(bookId, assetId, videoFilename);
261
+ return null;
262
+ }
263
+ function scheduleWaveformExtraction(bookId, assetId, videoFilename) {
264
+ const jobId = `extract-waveform-${bookId}-${assetId}`;
265
+ void queue
266
+ .enqueue(jobId, {
267
+ type: "extract_waveform",
268
+ bookId,
269
+ waveformAssetId: assetId,
270
+ videoFilename,
271
+ })
272
+ .catch((err) => {
273
+ logger.error("waveform-enqueue-failed", { assetId, error: err });
274
+ });
275
+ }
276
+ const PARTIAL_DOWNLOAD_RE = /\.(mp4|mov)\.part$/i;
277
+ /** Media extensions whose duration can be recovered via ffprobe when metadata lacks it. */
278
+ const DURATION_PROBE_EXT_RE = /\.(mp4|mov|webm|mkv|mp3|wav|m4a|aac|ogg|flac)$/i;
279
+ export const RENDER_ORIENTATIONS = MEDIA_RENDER_ORIENTATIONS;
280
+ export async function readRenderSettings(engine, assetId, orientations) {
281
+ const result = {};
282
+ await Promise.all(orientations.map(async (orient) => {
283
+ const meta = await engine.metadata.artifacts.read(assetId, `${orient}.render`);
284
+ if (meta.ok)
285
+ result[orient] = meta.value;
286
+ }));
287
+ return result;
288
+ }
289
+ const BASE_DEFAULTS = {
290
+ subtitlesEnabled: false,
291
+ subtitleLanguage: "original",
292
+ position: "bottom",
293
+ fontSize: "medium",
294
+ audioFadeIn: 0,
295
+ audioFadeOut: 0,
296
+ audioVolume: 100,
297
+ osgImagePath: null,
298
+ osgAssetId: null,
299
+ osgPosition: "top-right",
300
+ osgSize: "medium",
301
+ osgScale: 1,
302
+ osgOpacity: 1,
303
+ osgStartSeconds: null,
304
+ osgEndSeconds: null,
305
+ osgTailSeconds: null,
306
+ keywordGraphicsEnabled: false,
307
+ keywordGraphicsMaxCount: DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT,
308
+ keywordGraphicsLingerSeconds: DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS,
309
+ keywordGraphicsKeywords: null,
310
+ backgroundMode: "blur",
311
+ strokeWidth: 6,
312
+ strokeColor: "#000000",
313
+ shadowBlur: 6,
314
+ shadowOpacity: 60,
315
+ };
316
+ const ORIENTATION_OVERRIDES = {
317
+ portrait: {
318
+ highlightStyle: "impact",
319
+ textColor: "#FFFFFF",
320
+ highlightColor: "#FFFF00",
321
+ },
322
+ landscape: {
323
+ highlightStyle: "highlight",
324
+ textColor: "#FFFFFF",
325
+ highlightColor: "#991B1B",
326
+ },
327
+ square: {
328
+ highlightStyle: "highlight",
329
+ textColor: "#FFFFFF",
330
+ highlightColor: "#991B1B",
331
+ },
332
+ };
333
+ export async function readStoredRenderDefaults(engine, assetId, targetOrientation) {
334
+ const defaults = { ...BASE_DEFAULTS, ...(ORIENTATION_OVERRIDES[targetOrientation] ?? ORIENTATION_OVERRIDES.landscape) };
335
+ // Read every orientation's render metadata in parallel, then pick the
336
+ // target's own metadata first or fall back to any other orientation.
337
+ const tryOrder = [targetOrientation, ...RENDER_ORIENTATIONS.filter((o) => o !== targetOrientation)];
338
+ const metas = await Promise.all(tryOrder.map((orient) => engine.metadata.artifacts.read(assetId, `${orient}.render`)));
339
+ for (const meta of metas) {
340
+ if (meta.ok)
341
+ return { ...defaults, ...meta.value };
342
+ }
343
+ const recs = await readAnalysisRecommendations(engine, assetId);
344
+ return { ...defaults, ...recs };
345
+ }
346
+ const VALID_SUB_POSITIONS = new Set(["top", "middle", "bottom"]);
347
+ const VALID_OSG_POSITIONS = new Set([
348
+ "top-left", "top-center", "top-right",
349
+ "middle-left", "middle-center", "middle-right",
350
+ "bottom-left", "bottom-center", "bottom-right",
351
+ ]);
352
+ async function readAnalysisRecommendations(engine, assetId) {
353
+ const result = await engine.files.read(assetId, ".original.analysis.json");
354
+ if (!result.ok)
355
+ return {};
356
+ try {
357
+ const raw = typeof result.value === "string"
358
+ ? result.value
359
+ : new TextDecoder().decode(result.value);
360
+ const analysis = JSON.parse(raw);
361
+ const recs = {};
362
+ const subPos = analysis.recommendedSubtitlePosition;
363
+ if (typeof subPos === "string" && VALID_SUB_POSITIONS.has(subPos)) {
364
+ recs.position = subPos;
365
+ }
366
+ const osgPos = analysis.recommendedOsgPosition;
367
+ if (typeof osgPos === "string" && VALID_OSG_POSITIONS.has(osgPos)) {
368
+ recs.osgPosition = osgPos;
369
+ }
370
+ return recs;
371
+ }
372
+ catch {
373
+ return {};
374
+ }
375
+ }
376
+ // Content fingerprint covering every file in the asset directory. Changes
377
+ // whenever any file is added, removed, resized, or rewritten, so every URL
378
+ // derived from an asset's filePath (including alternate-orientation renders
379
+ // like portrait.mp4 / landscape.mp4) invalidates together. Cheap: no file
380
+ // reads — just hashes the immutable manifest tuples. Partial downloads are
381
+ // excluded: their size grows on every poll while no served URL points at
382
+ // them, and including them made every media URL churn (and the browser
383
+ // refetch) once per poll tick for the whole duration of a download.
384
+ function computeAssetFingerprint(files) {
385
+ const settled = files.filter((f) => !PARTIAL_DOWNLOAD_RE.test(f.name));
386
+ if (settled.length === 0)
387
+ return "";
388
+ const sorted = [...settled].sort((a, b) => a.name.localeCompare(b.name));
389
+ const hash = createHash("sha1");
390
+ for (const f of sorted) {
391
+ hash.update(f.name);
392
+ hash.update("\0");
393
+ hash.update(String(f.sizeBytes ?? 0));
394
+ hash.update("\0");
395
+ hash.update(f.objectHash);
396
+ hash.update("\0");
397
+ }
398
+ return hash.digest("hex").slice(0, 12);
399
+ }
400
+ function renderedOrientationsFor(files, kind) {
401
+ const fileSet = new Set(files.map((f) => f.name));
402
+ if (kind === "final") {
403
+ return RENDER_ORIENTATIONS.filter((o) => fileSet.has(`timeline_${o}.mp4`));
404
+ }
405
+ return RENDER_ORIENTATIONS.filter((o) => fileSet.has(`${o}.mp4`));
406
+ }
407
+ function indexByArtifactId(rows) {
408
+ const map = new Map();
409
+ for (const row of rows)
410
+ map.set(row.artifactId, row);
411
+ return map;
412
+ }
413
+ function groupByArtifactId(rows) {
414
+ const map = new Map();
415
+ for (const row of rows) {
416
+ const list = map.get(row.artifactId);
417
+ if (list)
418
+ list.push(row);
419
+ else
420
+ map.set(row.artifactId, [row]);
421
+ }
422
+ return map;
423
+ }
424
+ export async function buildAssetList(engine, book_id, sort) {
425
+ const assets = engine.artifacts.list({ ...(sort ? { sort } : {}) });
426
+ const pendingAll = engine.jobs.pending.findAll();
427
+ const failuresAll = engine.jobs.failures.findAll();
428
+ const workAll = engine.jobs.artifactWork.list();
429
+ const streamsAll = engine.streams.list();
430
+ const pendingByArtifact = indexByArtifactId(pendingAll.ok ? pendingAll.value : []);
431
+ const failuresByArtifact = indexByArtifactId(failuresAll.ok ? failuresAll.value : []);
432
+ const workByArtifact = indexByArtifactId(workAll.ok ? workAll.value : []);
433
+ const streamsByArtifact = groupByArtifactId(streamsAll.ok ? streamsAll.value : []);
434
+ const tagsByArtifact = readAssetTagSummaries(engine, assets.map((asset) => asset.artifactId));
435
+ const assetRows = await Promise.all(assets.map(async (a) => {
436
+ const [manifestResult, metadata, archiveMetadata, proxyMetadata] = await Promise.all([
437
+ engine.files.manifest(a.artifactId, { includeDotfiles: true }),
438
+ engine.metadata.artifacts.read(a.artifactId, "original"),
439
+ engine.metadata.artifacts.read(a.artifactId, "archive"),
440
+ a.kind === "video"
441
+ ? engine.metadata.artifacts.read(a.artifactId, "proxy")
442
+ : Promise.resolve({
443
+ ok: false,
444
+ error: { code: "NOT_FOUND", message: "not a video" },
445
+ }),
446
+ ]);
447
+ const lockData = engine.jobs.locks.get(a.path);
448
+ const pendingTask = {
449
+ ok: true,
450
+ value: pendingByArtifact.get(a.artifactId) ?? null,
451
+ };
452
+ const generationError = {
453
+ ok: true,
454
+ value: failuresByArtifact.get(a.artifactId) ?? null,
455
+ };
456
+ const assetWorkRow = {
457
+ ok: true,
458
+ value: workByArtifact.get(a.artifactId) ?? null,
459
+ };
460
+ // If the asset directory disappeared between listAssets and getManifest,
461
+ // surface an empty-but-error placeholder rather than throwing.
462
+ const files = manifestResult.ok ? manifestResult.value.files : [];
463
+ const directories = manifestResult.ok ? manifestResult.value.directories : undefined;
464
+ const mediaFile = findAssetMediaFile(files, a);
465
+ const meta = metadata.ok ? metadata.value : null;
466
+ const streams = streamsByArtifact.get(a.artifactId) ?? [];
467
+ const videoStream = streams.find((stream) => stream.kind === "video" && stream.video);
468
+ const audioStream = streams.find((stream) => stream.kind === "audio" && stream.audio);
469
+ const frameRate = videoStream?.video?.averageFrameRate
470
+ ?? videoStream?.video?.nominalFrameRate;
471
+ const streamDuration = videoStream
472
+ ? videoStream.durationTicks
473
+ * videoStream.timeBase.numerator
474
+ / videoStream.timeBase.denominator
475
+ : 0;
476
+ const metadataDuration = Number(meta?.duration);
477
+ const sourceDuration = streamDuration > 0
478
+ ? streamDuration
479
+ : Number.isFinite(metadataDuration) ? metadataDuration : 0;
480
+ const sourceProfile = sourceDuration > 0
481
+ ? {
482
+ videoDuration: sourceDuration,
483
+ avgFps: frameRate
484
+ ? frameRate.numerator / frameRate.denominator
485
+ : null,
486
+ hasAudio: audioStream !== undefined || meta?.hasAudio === true,
487
+ }
488
+ : null;
489
+ const fileNames = new Set(files.map((f) => f.name));
490
+ const hasThumbnail = fileNames.has("original.jpg");
491
+ const renders = renderedOrientationsFor(files, a.kind);
492
+ const hasPartFile = files.some((f) => PARTIAL_DOWNLOAD_RE.test(f.name));
493
+ const frameDirs = buildFrameDirs(directories, book_id, a.artifactId);
494
+ // Spoken-media assets: ship the transcript so the inspector can show a
495
+ // copiable transcript (and offer Transcribe when there is none) without a
496
+ // second round trip. One small sidecar read per eligible asset; word-timed
497
+ // transcripts resolve from the object-hash memo on repeat listings.
498
+ const hasTranscriptSidecar = files.some((file) => file.name === TIMED_TRANSCRIPT_FILE || file.name === PLAIN_TRANSCRIPT_FILE);
499
+ const [transcript, vision_analysis] = await Promise.all([
500
+ hasSpokenAudio(a.kind) && hasTranscriptSidecar
501
+ ? readAssetTranscriptSources(engine, a.artifactId, files)
502
+ : Promise.resolve(null),
503
+ isAnalyzableVideo(a.kind) && fileNames.has(VIDEO_ANALYSIS_FILE)
504
+ ? readVideoAnalysisSummary(engine, a.artifactId)
505
+ : Promise.resolve(null),
506
+ ]);
507
+ const waveform = await readWaveformOrSchedule(engine, a.artifactId, book_id, mediaFile?.name ?? null);
508
+ // Cache-bust media URLs so browsers refetch after trim / re-encode / re-render.
509
+ // For video and image assets, use a content fingerprint covering the whole
510
+ // asset directory: orientation re-renders (portrait/landscape/square.mp4)
511
+ // touch alternate files but not original.mp4, so a primary-mtime cache
512
+ // key would leave alternate-orientation URLs cached and stale.
513
+ const useFingerprint = a.kind === "video" || a.kind === "image";
514
+ let cacheBust = '';
515
+ if (useFingerprint) {
516
+ const fp = computeAssetFingerprint(files);
517
+ if (fp)
518
+ cacheBust = `?v=${fp}`;
519
+ }
520
+ else {
521
+ const mediaHash = mediaFile?.objectHash;
522
+ if (mediaHash)
523
+ cacheBust = `?v=${mediaHash.slice(0, 12)}`;
524
+ }
525
+ // Character assets: free-form folders have no original.* media. Project
526
+ // the first image in the folder so sidebar thumbs and drag-drop keep working.
527
+ let charImage = null;
528
+ if (a.kind === "character") {
529
+ charImage = files.find((f) => /\.(jpg|jpeg|png|webp|gif)$/i.test(f.name)) ?? null;
530
+ }
531
+ const proxyCandidate = a.kind === "video"
532
+ ? files.find((file) => file.name === "proxy.mp4")
533
+ : null;
534
+ let proxyFile = null;
535
+ if (proxyCandidate && mediaFile) {
536
+ if (proxyMetadata.ok
537
+ && videoProxyMetadataMatches(proxyMetadata.value, mediaFile, proxyCandidate)) {
538
+ proxyFile = proxyCandidate;
539
+ }
540
+ else {
541
+ const assessmentMatches = proxyMetadata.ok
542
+ && videoProxyAssessmentMatches(proxyMetadata.value, mediaFile, proxyCandidate);
543
+ const active = assetWorkRow.ok
544
+ && assetWorkRow.value !== null
545
+ && (assetWorkRow.value.status === "pending" || assetWorkRow.value.status === "working");
546
+ if (!assessmentMatches && !active && sourceProfile) {
547
+ const valid = await assessStoredVideoProxy(engine, a.artifactId, mediaFile, proxyCandidate, sourceProfile);
548
+ if (valid)
549
+ proxyFile = proxyCandidate;
550
+ }
551
+ }
552
+ }
553
+ const media_url = charImage
554
+ ? `/files/${book_id}/${a.artifactId}/${charImage.name}${cacheBust}`
555
+ : proxyFile
556
+ ? `/files/${book_id}/${a.artifactId}/${proxyFile.name}${cacheBust}`
557
+ : mediaFile
558
+ ? `/files/${book_id}/${a.artifactId}/${mediaFile.name}${cacheBust}`
559
+ : null;
560
+ const isImage = a.kind === "image";
561
+ const thumbnail_url = charImage
562
+ ? `/files/${book_id}/${a.artifactId}/${charImage.name}${cacheBust}`
563
+ : hasThumbnail
564
+ ? `/files/${book_id}/${a.artifactId}/original.jpg${cacheBust}`
565
+ : isImage ? media_url : null;
566
+ // Status derivation lives in videobook-engine (`computeArtifactStatus`). We pass
567
+ // the asset-type-aware primary media name we already computed so the
568
+ // engine doesn't have to redo media-type heuristics.
569
+ const status = computeArtifactStatus({
570
+ kind: a.kind,
571
+ fileNames,
572
+ primaryMediaName: mediaFile?.name ?? null,
573
+ hasOriginalMetadata: metadata.ok,
574
+ hasPartFile,
575
+ lockData,
576
+ pendingTask: pendingTask.ok ? pendingTask.value : null,
577
+ generationError: generationError.ok ? generationError.value : null,
578
+ artifactRow: assetWorkRow.ok && assetWorkRow.value
579
+ ? {
580
+ status: assetWorkRow.value.status,
581
+ meta: assetWorkRow.value.meta,
582
+ deadlineAt: assetWorkRow.value.deadlineAt,
583
+ }
584
+ : null,
585
+ });
586
+ if (a.kind === "video" && status === "ready" && !hasThumbnail && mediaFile) {
587
+ const recovery = await engine.metadata.artifacts.read(a.artifactId, "poster-recovery");
588
+ if (!recovery.ok || !recovery.value.failed || recovery.value.sourceHash !== mediaFile.objectHash) {
589
+ void queue.enqueue(`extract-video-poster-${book_id}-${a.artifactId}-${mediaFile.objectHash}`, {
590
+ type: "extract_video_poster",
591
+ bookId: book_id,
592
+ posterArtifactId: a.artifactId,
593
+ sourceFilename: mediaFile.name,
594
+ sourceHash: mediaFile.objectHash,
595
+ }).catch((error) => {
596
+ logger.error("poster-enqueue-failed", { assetId: a.artifactId, error });
597
+ });
598
+ }
599
+ }
600
+ const archiveMeta = archiveMetadata.ok ? archiveMetadata.value : null;
601
+ const analysisInfo = analysisInfoFromMetadata(meta);
602
+ if (analysisInfo && fileNames.has("analysis.json")) {
603
+ const output = await analysisOutputProjection(engine, a.artifactId);
604
+ if (output) {
605
+ analysisInfo.output_json = output.outputJson;
606
+ analysisInfo.output_url =
607
+ `/files/${book_id}/${a.artifactId}/analysis.json`;
608
+ analysisInfo.llm_exchanges = output.llmExchanges;
609
+ }
610
+ }
611
+ // Backfill duration if missing from metadata (video + audio; probeVideo
612
+ // reads format.duration via ffprobe and works for both).
613
+ let duration = meta?.duration ?? null;
614
+ if (duration === null && mediaFile && DURATION_PROBE_EXT_RE.test(mediaFile.name)) {
615
+ try {
616
+ const probe = await probeVideo(join(a.path, mediaFile.name));
617
+ if (Number.isFinite(probe.duration) && probe.duration > 0) {
618
+ duration = probe.duration;
619
+ // Persist so we don't re-probe next time
620
+ if (metadata.ok) {
621
+ await engine.metadata.artifacts.write(a.artifactId, "original", { ...metadata.value, duration });
622
+ }
623
+ }
624
+ }
625
+ catch { /* non-fatal */ }
626
+ }
627
+ const audio_info = audioStream?.audio
628
+ ? {
629
+ codec: audioStream.codec,
630
+ sample_rate_hz: audioStream.audio.sampleRateHz,
631
+ channels: audioStream.audio.channels,
632
+ channel_layout: audioStream.audio.channelLayout,
633
+ duration_seconds: audioStream.durationTicks
634
+ * audioStream.timeBase.numerator
635
+ / audioStream.timeBase.denominator,
636
+ }
637
+ : null;
638
+ // Character assets: surface whether an ElevenLabs voice is assigned, so the
639
+ // client can flag voiceless hosts before a Clip Show plan is attempted.
640
+ const has_voice = null;
641
+ // Video/final assets: surface the configured OSG overlay so the client can
642
+ // render its badge on clip cards and timeline slots without a per-asset
643
+ // settings fetch. Bounded to one metadata read per OSG-eligible asset.
644
+ const osg = (a.kind === "video" || a.kind === "final")
645
+ ? await readOsgSummary(engine, a.artifactId)
646
+ : null;
647
+ // Read script content from its canonical artifact file.
648
+ let prompt = meta?.prompt ?? null;
649
+ if (!prompt && a.kind === "script" && fileNames.has("original.md")) {
650
+ const md = await engine.files.read(a.artifactId, "original.md");
651
+ if (md.ok)
652
+ prompt = md.value.toString();
653
+ }
654
+ const assetWork = assetWorkRow.ok && assetWorkRow.value
655
+ ? {
656
+ status: assetWorkRow.value.status,
657
+ meta: assetWorkRow.value.meta,
658
+ ownerKind: assetWorkRow.value.ownerKind,
659
+ deadlineAt: assetWorkRow.value.deadlineAt,
660
+ }
661
+ : null;
662
+ return {
663
+ id: a.artifactId,
664
+ artifact_id: a.artifactId,
665
+ ...(a.label ? { label: a.label } : {}),
666
+ type: a.kind,
667
+ created_at: new Date(a.createdAt).toISOString(),
668
+ path: a.path,
669
+ media_url,
670
+ thumbnail_url,
671
+ object_hash: mediaFile?.objectHash ?? null,
672
+ status,
673
+ assetWork,
674
+ width: meta?.width ?? null,
675
+ height: meta?.height ?? null,
676
+ origin: meta?.origin ?? null,
677
+ prompt,
678
+ renders,
679
+ duration,
680
+ fps: (() => {
681
+ const metadataFps = Number(meta?.fps);
682
+ if (Number.isFinite(metadataFps) && metadataFps > 0)
683
+ return metadataFps;
684
+ if (frameRate && frameRate.denominator > 0) {
685
+ const streamFps = frameRate.numerator / frameRate.denominator;
686
+ return Number.isFinite(streamFps) && streamFps > 0 ? streamFps : null;
687
+ }
688
+ return null;
689
+ })(),
690
+ audio_info,
691
+ analysis_info: analysisInfo,
692
+ tags: tagsByArtifact.get(a.artifactId) ?? EMPTY_TAG_SUMMARY,
693
+ transcript,
694
+ vision_analysis,
695
+ waveform,
696
+ frame_dirs: frameDirs,
697
+ archive_id: typeof archiveMeta?.archive_id === "string" ? archiveMeta.archive_id : null,
698
+ archived_at: typeof archiveMeta?.archived_at === "string" ? archiveMeta.archived_at : null,
699
+ seen_at: assetWorkRow.ok && assetWorkRow.value ? assetWorkRow.value.seenAt : null,
700
+ has_voice,
701
+ osg,
702
+ };
703
+ }));
704
+ const notebookRows = engine.notebooks
705
+ .list()
706
+ .map((notebook) => ({
707
+ id: notebook.id,
708
+ artifact_id: notebook.id,
709
+ label: notebook.name,
710
+ type: "notebook",
711
+ created_at: notebook.createdAt,
712
+ path: `notebook:${notebook.id}`,
713
+ media_url: null,
714
+ thumbnail_url: null,
715
+ object_hash: null,
716
+ status: "ready",
717
+ assetWork: null,
718
+ width: null,
719
+ height: null,
720
+ origin: "notebook",
721
+ prompt: typeof notebook.description === "string"
722
+ ? notebook.description
723
+ : notebook.name,
724
+ renders: [],
725
+ duration: null,
726
+ fps: null,
727
+ audio_info: null,
728
+ analysis_info: null,
729
+ tags: null,
730
+ transcript: null,
731
+ vision_analysis: null,
732
+ waveform: null,
733
+ frame_dirs: {},
734
+ archive_id: null,
735
+ archived_at: null,
736
+ seen_at: null,
737
+ has_voice: null,
738
+ osg: null,
739
+ }));
740
+ const direction = sort === "oldest" ? 1 : -1;
741
+ return [...assetRows, ...notebookRows].sort((left, right) => direction *
742
+ (Date.parse(left.created_at) - Date.parse(right.created_at)));
743
+ }
744
+ /**
745
+ * Poll provider-pending tasks: submit queued KIE rows, age out stale tasks,
746
+ * and poll kie/veo/xai/alibaba/fal recordInfo lanes that mark
747
+ * completing + enqueue complete_kie_task. Does NOT process external uploads,
748
+ * auto-render, or auto-transcribe.
749
+ */
750
+ export async function pollPendingProviderTasks(engine, book_id, options = {}) {
751
+ if (currentBookContext()?.operation !== "poll_pending_provider_tasks") {
752
+ return withBookContext({ bookId: book_id, engine, operation: "poll_pending_provider_tasks" }, () => pollPendingProviderTasks(engine, book_id, options));
753
+ }
754
+ await settleFinishedUsage(engine);
755
+ const result = {
756
+ pending: [],
757
+ completed: [],
758
+ failed: [],
759
+ };
760
+ if (options.signal?.aborted)
761
+ return result;
762
+ const tasksRes = await engine.jobs.pending.findAll();
763
+ const tasks = tasksRes.ok ? tasksRes.value : [];
764
+ if (tasks.length === 0)
765
+ return result;
766
+ const kieBaseURL = options.kieBaseURL ?? providerEndpoints().kieBaseURL;
767
+ const falQueueBaseURL = options.falQueueBaseURL ?? providerEndpoints().falQueueBaseURL;
768
+ // Age out any non-completing task older than timeout so it
769
+ // stops showing as pending forever. Completing tasks are skipped because the
770
+ // completion job is already dispatched; recoverAssetsTable handles that lane
771
+ // on server restart by clearing the completing flag and refreshing owner_id.
772
+ const timeoutMs = options.timeoutMs ?? PENDING_TASK_TIMEOUT_MS;
773
+ const nowMs = options.nowMs ?? Date.now();
774
+ const activeTasks = [];
775
+ const expiredTasks = [];
776
+ for (const originalTask of tasks) {
777
+ let t = originalTask;
778
+ if (isQueuedKiePendingTask(t)) {
779
+ try {
780
+ t = await withBookContext({ bookId: book_id, engine, operation: "submit_queued_kie_task", artifactId: t.artifactId }, () => submitQueuedKieTask(engine, t));
781
+ }
782
+ catch (e) {
783
+ const failMsg = e instanceof Error ? e.message : "Queued KIE submit failed";
784
+ const prompt = metaPrompt(t.meta);
785
+ result.failed.push({ assetId: t.artifactId, taskType: t.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
786
+ await failPendingTaskAndLog(engine, t.artifactId, t.taskId, { message: failMsg, prompt });
787
+ continue;
788
+ }
789
+ }
790
+ const ageMs = nowMs - t.createdAt;
791
+ if (t.completing !== true && Number.isFinite(ageMs) && ageMs > timeoutMs) {
792
+ expiredTasks.push(t);
793
+ }
794
+ activeTasks.push(t);
795
+ }
796
+ // Completing tasks — reconciliation already enqueued a completion job for these on a
797
+ // prior poll. Skip polling + re-enqueueing so duplicate completion jobs
798
+ // don't race on the asset-directory lock. Report as pending with state
799
+ // "success" so the inbox tile shows the DOWNLOAD/PROCESS phase.
800
+ const completingTasks = activeTasks.filter((t) => t.completing === true);
801
+ for (const t of completingTasks) {
802
+ result.pending.push({ assetId: t.artifactId, taskType: t.taskType, progress: 100, state: "success", prompt: metaPrompt(t.meta), startedAt: t.createdAt });
803
+ }
804
+ const pollableTasks = activeTasks.filter((t) => t.completing !== true);
805
+ if (options.signal?.aborted)
806
+ return result;
807
+ await Promise.all(pollableTasks.map(async (task) => {
808
+ const ownerId = await engine.jobs.pending.getOwner(task.artifactId, task.taskId);
809
+ if (ownerId) {
810
+ await engine.jobs.artifactWork.renew(task.artifactId, ownerId, 30 * 60_000);
811
+ }
812
+ }));
813
+ // Poll KIE tasks (only if API key is set)
814
+ const kieTasks = pollableTasks.filter(isKiePendingTask);
815
+ const kieApiKey = getApiKey("KIE_API_KEY");
816
+ if (kieApiKey && kieTasks.length > 0) {
817
+ const provider = createKie({
818
+ apiKey: kieApiKey,
819
+ timeout: 30_000,
820
+ ...(kieBaseURL ? { baseURL: kieBaseURL } : {}),
821
+ });
822
+ await Promise.all(kieTasks.map(async (task) => {
823
+ try {
824
+ const taskInfo = await provider.get.api.v1.jobs.recordInfo(task.taskId);
825
+ await processKieTaskResult(engine, task, taskInfo, result, book_id);
826
+ }
827
+ catch (e) {
828
+ const failMsg = e instanceof Error ? e.message : "Poll failed";
829
+ const prompt = metaPrompt(task.meta);
830
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
831
+ if (e instanceof KieError) {
832
+ // TS narrowing on `e` is unreliable here, use an explicit assertion.
833
+ const kieErr = e;
834
+ if (kieErr.status >= 400 && kieErr.status < 500) {
835
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
836
+ }
837
+ }
838
+ }
839
+ }));
840
+ }
841
+ // Poll KIE Veo tasks (separate namespace: kie.veo.get.api.v1.veo.recordInfo)
842
+ const veoTasks = pollableTasks.filter(isVeoPendingTask);
843
+ if (kieApiKey && veoTasks.length > 0) {
844
+ const provider = createKie({
845
+ apiKey: kieApiKey,
846
+ timeout: 30_000,
847
+ ...(kieBaseURL ? { baseURL: kieBaseURL } : {}),
848
+ });
849
+ await Promise.all(veoTasks.map(async (task) => {
850
+ const prompt = metaPrompt(task.meta);
851
+ try {
852
+ const recordInfo = await provider.veo.get.api.v1.veo.recordInfo(task.taskId);
853
+ const data = recordInfo.data;
854
+ // Veo successFlag: 0=submitted/pending, 1=success, 2=fail, 3=processing
855
+ if (data?.successFlag === 1) {
856
+ const urls = data.response?.resultUrls ?? [];
857
+ if (urls.length === 0) {
858
+ const failMsg = "Veo task succeeded but returned no resultUrls";
859
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
860
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
861
+ return;
862
+ }
863
+ const assetDirResult = await engine.workspaces.resolveArtifact(task.artifactId);
864
+ if (!assetDirResult.ok) {
865
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: `Asset not found: ${task.artifactId}`, prompt });
866
+ return;
867
+ }
868
+ await engine.jobs.pending.markCompleting(task.artifactId);
869
+ const jobId = `complete-veo-${task.artifactId}-${Date.now()}`;
870
+ await queue.enqueue(jobId, {
871
+ type: "complete_kie_task",
872
+ taskId: task.taskId,
873
+ taskType: task.taskType,
874
+ assetId: task.artifactId,
875
+ assetDir: assetDirResult.value,
876
+ bookId: book_id,
877
+ meta: task.meta,
878
+ resultUrls: urls,
879
+ });
880
+ result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
881
+ }
882
+ else if (data?.successFlag === 2) {
883
+ const failMsg = data.errorMessage ?? "Veo task failed";
884
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
885
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
886
+ }
887
+ else {
888
+ result.pending.push({
889
+ assetId: task.artifactId,
890
+ taskType: task.taskType,
891
+ progress: 0,
892
+ state: data?.successFlag === 3 ? "processing" : "waiting",
893
+ prompt,
894
+ startedAt: task.createdAt,
895
+ });
896
+ }
897
+ }
898
+ catch (e) {
899
+ const failMsg = e instanceof Error ? e.message : "Veo poll failed";
900
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
901
+ if (e instanceof KieError) {
902
+ const kieErr = e;
903
+ if (kieErr.status >= 400 && kieErr.status < 500) {
904
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
905
+ }
906
+ }
907
+ }
908
+ }));
909
+ }
910
+ // Poll xAI tasks (only if API key is set)
911
+ const xaiTasks = pollableTasks.filter((t) => t.taskType.startsWith("xai_"));
912
+ if (process.env.XAI_API_KEY && xaiTasks.length > 0) {
913
+ const provider = createXai({ apiKey: process.env.XAI_API_KEY, timeout: 30_000 });
914
+ await Promise.all(xaiTasks.map(async (task) => {
915
+ const prompt = metaPrompt(task.meta);
916
+ try {
917
+ const videoResult = await provider.get.v1.videos(task.taskId);
918
+ if (videoResult.status === "done" && videoResult.video?.url) {
919
+ const assetDirResult = await engine.workspaces.resolveArtifact(task.artifactId);
920
+ if (!assetDirResult.ok) {
921
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: `Asset not found: ${task.artifactId}`, prompt });
922
+ return;
923
+ }
924
+ await engine.jobs.pending.markCompleting(task.artifactId);
925
+ const jobId = `complete-xai-${task.artifactId}-${Date.now()}`;
926
+ await queue.enqueue(jobId, {
927
+ type: "complete_kie_task",
928
+ taskId: task.taskId,
929
+ taskType: task.taskType,
930
+ assetId: task.artifactId,
931
+ assetDir: assetDirResult.value,
932
+ bookId: book_id,
933
+ meta: task.meta,
934
+ resultUrls: [videoResult.video.url],
935
+ });
936
+ result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
937
+ }
938
+ else if (videoResult.status === "failed" || videoResult.status === "expired") {
939
+ const failMsg = `xAI task ${videoResult.status}`;
940
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
941
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
942
+ }
943
+ else {
944
+ result.pending.push({
945
+ assetId: task.artifactId,
946
+ taskType: task.taskType,
947
+ progress: videoResult.progress ?? 0,
948
+ state: videoResult.status,
949
+ prompt,
950
+ startedAt: task.createdAt,
951
+ });
952
+ }
953
+ }
954
+ catch (e) {
955
+ const failMsg = e instanceof Error ? e.message : "Poll failed";
956
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
957
+ if (e instanceof XaiError) {
958
+ const xaiErr = e;
959
+ if (xaiErr.status >= 400 && xaiErr.status < 500) {
960
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
961
+ }
962
+ }
963
+ }
964
+ }));
965
+ }
966
+ // Poll Alibaba tasks (only if API key is set)
967
+ const alibabaTasks = pollableTasks.filter((t) => t.taskType.startsWith("alibaba_"));
968
+ if (process.env.DASHSCOPE_API_KEY && alibabaTasks.length > 0) {
969
+ const provider = createAlibaba({ apiKey: process.env.DASHSCOPE_API_KEY, timeout: 30_000 });
970
+ await Promise.all(alibabaTasks.map(async (task) => {
971
+ const prompt = metaPrompt(task.meta);
972
+ try {
973
+ const taskStatus = await provider.get.api.v1.tasks(task.taskId);
974
+ const status = taskStatus.output.task_status;
975
+ if (status === "SUCCEEDED") {
976
+ const assetDirResult = await engine.workspaces.resolveArtifact(task.artifactId);
977
+ if (!assetDirResult.ok) {
978
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: `Asset not found: ${task.artifactId}`, prompt });
979
+ return;
980
+ }
981
+ await engine.jobs.pending.markCompleting(task.artifactId);
982
+ const jobId = `complete-alibaba-${task.artifactId}-${Date.now()}`;
983
+ // Video result has video_url, image result has choices with image URLs
984
+ const resultUrls = [];
985
+ if (taskStatus.output.video_url) {
986
+ resultUrls.push(taskStatus.output.video_url);
987
+ }
988
+ if (taskStatus.output.choices) {
989
+ for (const choice of taskStatus.output.choices) {
990
+ for (const content of choice.message.content) {
991
+ if (content.type === "image" && content.image) {
992
+ resultUrls.push(content.image);
993
+ }
994
+ }
995
+ }
996
+ }
997
+ await queue.enqueue(jobId, {
998
+ type: "complete_kie_task",
999
+ taskId: task.taskId,
1000
+ taskType: task.taskType,
1001
+ assetId: task.artifactId,
1002
+ assetDir: assetDirResult.value,
1003
+ bookId: book_id,
1004
+ meta: task.meta,
1005
+ resultUrls,
1006
+ });
1007
+ result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
1008
+ }
1009
+ else if (status === "FAILED" || status === "CANCELED") {
1010
+ const failMsg = taskStatus.output.message ?? `Alibaba task ${status}`;
1011
+ const code = taskStatus.output.code;
1012
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
1013
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, {
1014
+ message: failMsg,
1015
+ prompt,
1016
+ ...(code !== undefined ? { failCode: code } : {}),
1017
+ });
1018
+ }
1019
+ else {
1020
+ result.pending.push({
1021
+ assetId: task.artifactId,
1022
+ taskType: task.taskType,
1023
+ progress: 0,
1024
+ state: status.toLowerCase(),
1025
+ prompt,
1026
+ startedAt: task.createdAt,
1027
+ });
1028
+ }
1029
+ }
1030
+ catch (e) {
1031
+ const failMsg = e instanceof Error ? e.message : "Poll failed";
1032
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
1033
+ if (e instanceof AlibabaError) {
1034
+ const alibabaErr = e;
1035
+ if (alibabaErr.status >= 400 && alibabaErr.status < 500) {
1036
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
1037
+ }
1038
+ }
1039
+ }
1040
+ }));
1041
+ }
1042
+ // Poll FAL tasks (only if API key is set)
1043
+ const falTasks = pollableTasks.filter((t) => t.taskType.startsWith("fal_"));
1044
+ if (process.env.FAL_API_KEY && falTasks.length > 0) {
1045
+ const provider = createFal({ apiKey: process.env.FAL_API_KEY, timeout: 30_000, ...(falQueueBaseURL ? { queueBaseURL: falQueueBaseURL } : {}) });
1046
+ // FAL's queue status/result endpoints live under the app namespace
1047
+ // (first 2 path segments). Hitting them with the full nested endpoint id
1048
+ // — e.g. `fal-ai/elevenlabs/speech-to-text/scribe-v2` — 405s. Strip back
1049
+ // to `fal-ai/elevenlabs` for polling.
1050
+ const falNamespace = (endpointId) => endpointId.split("/").slice(0, 2).join("/");
1051
+ await Promise.all(falTasks.map(async (task) => {
1052
+ const prompt = metaPrompt(task.meta);
1053
+ const endpointId = typeof task.meta?.falEndpointId === "string" ? task.meta.falEndpointId : null;
1054
+ if (!endpointId) {
1055
+ const failMsg = `FAL task missing falEndpointId in meta`;
1056
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
1057
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
1058
+ return;
1059
+ }
1060
+ const statusEndpointId = falNamespace(endpointId);
1061
+ try {
1062
+ const status = await provider.v1.queue.status({ endpoint_id: statusEndpointId, request_id: task.taskId });
1063
+ if (status.status === "COMPLETED") {
1064
+ if (status.error) {
1065
+ const failMsg = status.error;
1066
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
1067
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
1068
+ return;
1069
+ }
1070
+ const assetDirResult = await engine.workspaces.resolveArtifact(task.artifactId);
1071
+ if (!assetDirResult.ok) {
1072
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: `Asset not found: ${task.artifactId}`, prompt });
1073
+ return;
1074
+ }
1075
+ const resultObject = await provider.v1.queue.result({ endpoint_id: statusEndpointId, request_id: task.taskId });
1076
+ // Mark the pending task as completing BEFORE enqueueing so any
1077
+ // overlapping reconciliation poll skips it instead of enqueueing a duplicate.
1078
+ await engine.jobs.pending.markCompleting(task.artifactId);
1079
+ const jobId = `complete-fal-${task.artifactId}-${Date.now()}`;
1080
+ await queue.enqueue(jobId, {
1081
+ type: "complete_kie_task",
1082
+ taskId: task.taskId,
1083
+ taskType: task.taskType,
1084
+ assetId: task.artifactId,
1085
+ assetDir: assetDirResult.value,
1086
+ bookId: book_id,
1087
+ meta: task.meta,
1088
+ resultUrls: [],
1089
+ resultObject,
1090
+ });
1091
+ result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
1092
+ }
1093
+ else {
1094
+ result.pending.push({
1095
+ assetId: task.artifactId,
1096
+ taskType: task.taskType,
1097
+ progress: 0,
1098
+ state: status.status.toLowerCase(),
1099
+ prompt,
1100
+ startedAt: task.createdAt,
1101
+ });
1102
+ }
1103
+ }
1104
+ catch (e) {
1105
+ const failMsg = describeFalError(e);
1106
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
1107
+ if (e instanceof FalError) {
1108
+ const falErr = e;
1109
+ if (falErr.status >= 400 && falErr.status < 500) {
1110
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
1111
+ }
1112
+ }
1113
+ }
1114
+ }));
1115
+ }
1116
+ for (const task of expiredTasks) {
1117
+ if (options.signal?.aborted)
1118
+ break;
1119
+ const current = engine.jobs.pending.read(task.artifactId);
1120
+ if (!current.ok || !current.value || current.value.taskId !== task.taskId
1121
+ || current.value.completing)
1122
+ continue;
1123
+ const ageMinutes = Math.round((nowMs - task.createdAt) / 60_000);
1124
+ const failMsg = `Task timed out after ${ageMinutes}m without reaching success or fail state`;
1125
+ const prompt = metaPrompt(task.meta);
1126
+ result.pending = result.pending.filter((entry) => entry.assetId !== task.artifactId);
1127
+ result.failed = result.failed.filter((entry) => entry.assetId !== task.artifactId);
1128
+ result.failed.push({ assetId: task.artifactId, taskType: task.taskType, failMsg, failedAt: new Date().toISOString(), prompt });
1129
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: failMsg, prompt });
1130
+ }
1131
+ return result;
1132
+ }
1133
+ export async function reconcileProjectWork(engine, book_id) {
1134
+ const result = {
1135
+ pending: [], completed: [], failed: [],
1136
+ external_uploads: [], auto_transcribe: [], auto_plain_transcript: [],
1137
+ };
1138
+ // Single listAssets call, shared by all consumers
1139
+ const assets = await engine.artifacts.list();
1140
+ // Run discovery steps in parallel with provider polling
1141
+ const [uploads, providerResult, transcribeActions, plainTranscriptActions, failuresRes] = await Promise.all([
1142
+ findUnprocessedUploads(engine, assets),
1143
+ pollPendingProviderTasks(engine, book_id),
1144
+ findAutoTranscribeActions(engine, assets),
1145
+ findAutoPlainTranscriptActions(engine, assets),
1146
+ engine.jobs.failures.findAll(),
1147
+ ]);
1148
+ const persistedFailures = failuresRes.ok ? failuresRes.value : [];
1149
+ // Process external uploads
1150
+ for (const upload of uploads) {
1151
+ await processExternalUpload(engine, upload, book_id);
1152
+ result.external_uploads.push({ assetId: upload.assetId, mediaFilename: upload.mediaFilename });
1153
+ }
1154
+ result.pending.push(...providerResult.pending);
1155
+ result.completed.push(...providerResult.completed);
1156
+ result.failed.push(...providerResult.failed);
1157
+ const tasksRes = await engine.jobs.pending.findAll();
1158
+ const tasks = tasksRes.ok ? tasksRes.value : [];
1159
+ // Merge persisted runtime failures that weren't already
1160
+ // reported by this poll and don't have an active pending task (retry in progress).
1161
+ const reportedAssetIds = new Set(result.failed.map((f) => f.assetId));
1162
+ const pendingAssetIds = new Set(tasks.map((t) => t.artifactId));
1163
+ for (const failure of persistedFailures) {
1164
+ if (!reportedAssetIds.has(failure.artifactId) && !pendingAssetIds.has(failure.artifactId)) {
1165
+ const asset = assets.find(a => a.artifactId === failure.artifactId);
1166
+ if (asset) {
1167
+ const manifestResult = await engine.files.manifest(failure.artifactId);
1168
+ if (manifestResult.ok) {
1169
+ const files = manifestResult.value.files;
1170
+ let hasMedia = false;
1171
+ if (asset.kind === "character") {
1172
+ hasMedia = files.some(f => /\\.(jpg|jpeg|png|webp|gif)$/i.test(f.name));
1173
+ }
1174
+ else {
1175
+ hasMedia = findAssetMediaFile(files, asset) !== null;
1176
+ }
1177
+ if (hasMedia) {
1178
+ await engine.jobs.failures.clear(failure.artifactId);
1179
+ continue;
1180
+ }
1181
+ }
1182
+ }
1183
+ result.failed.push({
1184
+ assetId: failure.artifactId,
1185
+ taskType: "",
1186
+ failMsg: failure.message,
1187
+ failedAt: new Date(failure.failedAt).toISOString(),
1188
+ prompt: failure.prompt ?? null,
1189
+ });
1190
+ }
1191
+ }
1192
+ // Auto-render and auto-analyze are intentionally NOT performed here.
1193
+ // Renders fire from explicit /render or assign_slot (lazy on slot drop).
1194
+ // Gemini analysis fires from assign_slot (lazy on slot drop) and
1195
+ // OpenAI plain speech-to-text fires eagerly from process_video on upload,
1196
+ // with a revision-scoped backfill here for processed videos that missed
1197
+ // .analysis.gpt4otrans.json. Reconciliation still retries ElevenLabs
1198
+ // (auto_transcribe) only after .original.analysis.json exists.
1199
+ // Auto-pipeline: advance analyzed videos through transcription. Stable
1200
+ // dedupe key, shared with analyze-video's auto-transcribe trigger: racing
1201
+ // enqueues collapse onto one queued job instead of transcribing twice.
1202
+ for (const action of transcribeActions) {
1203
+ const jobId = `auto-transcribe-${action.assetId}`;
1204
+ await queue.enqueue(jobId, {
1205
+ type: "transcribe",
1206
+ audioPath: action.audioPath,
1207
+ audioFilename: action.audioFilename,
1208
+ tagAudioEvents: true,
1209
+ diarize: true,
1210
+ assetId: action.assetId,
1211
+ assetDir: action.assetDir,
1212
+ bookId: book_id,
1213
+ });
1214
+ result.auto_transcribe.push({ assetId: action.assetId, jobId });
1215
+ }
1216
+ for (const action of plainTranscriptActions) {
1217
+ const planned = planVideoTranscriptJobs({
1218
+ skipDownstreamJobs: false,
1219
+ hasAudio: true,
1220
+ assetId: action.assetId,
1221
+ assetDir: action.assetDir,
1222
+ bookId: book_id,
1223
+ videoFilename: action.videoFilename,
1224
+ identity: action.revision,
1225
+ hasOpenAiKey: true,
1226
+ hasElevenLabsKey: false,
1227
+ });
1228
+ for (const job of planned) {
1229
+ await queue.enqueue(job.jobId, job.payload);
1230
+ result.auto_plain_transcript.push({ assetId: action.assetId, jobId: job.jobId });
1231
+ }
1232
+ }
1233
+ return result;
1234
+ }
1235
+ export function reconcileProjectWorkDef(engine) {
1236
+ return {
1237
+ name: "reconcile_project_work",
1238
+ description: "Finish pending provider tasks for this book and pick up files added outside the app (Finder or CLI) that need processing.",
1239
+ schema: {
1240
+ book_id: z.string().describe("Book id"),
1241
+ },
1242
+ handler: async (args) => {
1243
+ const { book_id } = args;
1244
+ const reconcileResult = await reconcileProjectWork(engine, book_id);
1245
+ return {
1246
+ content: [{ type: "text", text: JSON.stringify(reconcileResult) }],
1247
+ };
1248
+ },
1249
+ };
1250
+ }
1251
+ async function processKieTaskResult(engine, task, taskInfo, result, bookId) {
1252
+ const data = taskInfo.data;
1253
+ const prompt = metaPrompt(task.meta);
1254
+ if (data?.state === "success") {
1255
+ const assetDirResult = await engine.workspaces.resolveArtifact(task.artifactId);
1256
+ if (!assetDirResult.ok) {
1257
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, { message: `Asset not found: ${task.artifactId}`, prompt });
1258
+ return;
1259
+ }
1260
+ const jobId = `complete-${task.taskType}-${task.artifactId}-${Date.now()}`;
1261
+ const parsed = JSON.parse(data.resultJson ?? "{}");
1262
+ // Mark the pending task as completing BEFORE enqueueing so any
1263
+ // overlapping reconciliation poll skips it instead of enqueueing a duplicate.
1264
+ await engine.jobs.pending.markCompleting(task.artifactId);
1265
+ await queue.enqueue(jobId, {
1266
+ type: "complete_kie_task",
1267
+ taskId: task.taskId,
1268
+ taskType: task.taskType,
1269
+ assetId: task.artifactId,
1270
+ assetDir: assetDirResult.value,
1271
+ bookId,
1272
+ meta: task.meta,
1273
+ resultUrls: parsed.resultUrls ?? [],
1274
+ resultObject: parsed.resultObject,
1275
+ });
1276
+ result.completed.push({ assetId: task.artifactId, taskType: task.taskType, jobId });
1277
+ }
1278
+ else if (data?.state === "fail") {
1279
+ const failMsg = data.failMsg ?? "unknown error";
1280
+ // A kie 500 is only retryable while the task is still in flight; once it
1281
+ // has terminally completed, surface the failure instead of stranding the
1282
+ // asset as "working" until the 30m reconciliation timeout. See isTransientKieFailure.
1283
+ if (isTransientKieFailure(data)) {
1284
+ result.pending.push({
1285
+ assetId: task.artifactId,
1286
+ taskType: task.taskType,
1287
+ progress: data.progress ?? 0,
1288
+ state: data.state ?? "waiting",
1289
+ prompt,
1290
+ startedAt: task.createdAt,
1291
+ });
1292
+ return;
1293
+ }
1294
+ result.failed.push({
1295
+ assetId: task.artifactId,
1296
+ taskType: task.taskType,
1297
+ failMsg,
1298
+ failedAt: new Date().toISOString(),
1299
+ prompt,
1300
+ });
1301
+ await failPendingTaskAndLog(engine, task.artifactId, task.taskId, {
1302
+ message: failMsg,
1303
+ prompt,
1304
+ ...(data.failCode !== undefined ? { failCode: data.failCode } : {}),
1305
+ });
1306
+ }
1307
+ else {
1308
+ result.pending.push({
1309
+ assetId: task.artifactId,
1310
+ taskType: task.taskType,
1311
+ progress: data?.progress ?? 0,
1312
+ state: data?.state ?? "waiting",
1313
+ prompt,
1314
+ startedAt: task.createdAt,
1315
+ });
1316
+ }
1317
+ }
1318
+ export async function findEligibleRenderActions(engine, assets, orientation) {
1319
+ const candidates = await Promise.all(assets.filter((a) => a.kind === "video").map(async (asset) => {
1320
+ const manifestResult = await engine.files.manifest(asset.artifactId);
1321
+ if (!manifestResult.ok)
1322
+ return null;
1323
+ const manifest = manifestResult.value;
1324
+ const files = manifest.files;
1325
+ const fileNames = new Set(files.map((f) => f.name));
1326
+ const [analysis, transcription] = await Promise.all([
1327
+ engine.files.read(asset.artifactId, ".original.analysis.json"),
1328
+ engine.files.read(asset.artifactId, ".analysis.gpt4otrans.json"),
1329
+ ]);
1330
+ // Skip: already rendered for target orientation
1331
+ const renderMetadata = await engine.metadata.artifacts.read(asset.artifactId, `${orientation}.render`);
1332
+ if (renderMetadata.ok && typeof renderMetadata.value.renderedAt === "string")
1333
+ return null;
1334
+ // Skip: process_video incomplete
1335
+ if (!analysis.ok)
1336
+ return null;
1337
+ // Skip if speech-to-text analysis not yet complete (unless video has no audio)
1338
+ if (!transcription.ok) {
1339
+ const meta = await engine.metadata.artifacts.read(asset.artifactId, "original");
1340
+ if (meta.ok && meta.value.hasAudio)
1341
+ return null;
1342
+ }
1343
+ // For videos with dialog, wait for ElevenLabs transcription
1344
+ const dialogQuality = await readAndAssessDialog(engine, asset.artifactId);
1345
+ if (dialogQuality.hasDialog && !fileNames.has("elevenlabs.json"))
1346
+ return null;
1347
+ const videoFile = findVideoFile(files);
1348
+ if (videoFile && !await engine.jobs.locks.isLocked(manifest.path)) {
1349
+ return {
1350
+ assetId: asset.artifactId,
1351
+ assetDir: manifest.path,
1352
+ videoPath: join(manifest.path, videoFile.name),
1353
+ videoFilename: videoFile.name,
1354
+ hasElevenlabs: fileNames.has("elevenlabs.json"),
1355
+ };
1356
+ }
1357
+ return null;
1358
+ }));
1359
+ return candidates.filter((c) => c !== null);
1360
+ }
1361
+ async function findAutoTranscribeActions(engine, assets) {
1362
+ if (!process.env.ELEVENLABS_API_KEY)
1363
+ return [];
1364
+ const candidates = await Promise.all(assets.filter((a) => a.kind === "video").map(async (asset) => {
1365
+ const [manifestResult, locked, errorRow] = await Promise.all([
1366
+ engine.files.manifest(asset.artifactId),
1367
+ engine.jobs.locks.isLocked(asset.path),
1368
+ engine.jobs.failures.read(asset.artifactId),
1369
+ ]);
1370
+ if (!manifestResult.ok)
1371
+ return null;
1372
+ const manifest = manifestResult.value;
1373
+ const files = manifest.files;
1374
+ const fileNames = new Set(files.map((f) => f.name));
1375
+ // Must have completed analysis
1376
+ if (!(await engine.files.read(asset.artifactId, ".original.analysis.json")).ok)
1377
+ return null;
1378
+ // Skip if already transcribed
1379
+ if (fileNames.has("elevenlabs.json"))
1380
+ return null;
1381
+ // Skip if a prior transcription attempt failed — do not auto-retry.
1382
+ // User must manually clear the error to retry.
1383
+ if (errorRow.ok && errorRow.value !== null)
1384
+ return null;
1385
+ // Skip if locked
1386
+ if (locked)
1387
+ return null;
1388
+ // Must have real dialog
1389
+ const dialogQuality = await readAndAssessDialog(engine, asset.artifactId);
1390
+ if (!dialogQuality.hasDialog)
1391
+ return null;
1392
+ // Must have an audio file
1393
+ const audioFile = findAudioFile(files, asset.kind);
1394
+ if (!audioFile)
1395
+ return null;
1396
+ return {
1397
+ assetId: asset.artifactId,
1398
+ assetDir: manifest.path,
1399
+ audioPath: join(manifest.path, audioFile.name),
1400
+ audioFilename: audioFile.name,
1401
+ };
1402
+ }));
1403
+ return candidates.filter((c) => c !== null);
1404
+ }
1405
+ async function findAutoPlainTranscriptActions(engine, assets) {
1406
+ if (!process.env.OPENAI_API_KEY)
1407
+ return [];
1408
+ const candidates = await Promise.all(assets.filter((a) => a.kind === "video").map(async (asset) => {
1409
+ const [manifestResult, locked, errorRow, originalMeta] = await Promise.all([
1410
+ engine.files.manifest(asset.artifactId),
1411
+ engine.jobs.locks.isLocked(asset.path),
1412
+ engine.jobs.failures.read(asset.artifactId),
1413
+ engine.metadata.artifacts.read(asset.artifactId, "original"),
1414
+ ]);
1415
+ if (!manifestResult.ok)
1416
+ return null;
1417
+ if (!originalMeta.ok)
1418
+ return null;
1419
+ if (originalMeta.value.hasAudio === false)
1420
+ return null;
1421
+ if (errorRow.ok && errorRow.value !== null)
1422
+ return null;
1423
+ if (locked)
1424
+ return null;
1425
+ const manifest = manifestResult.value;
1426
+ const files = manifest.files;
1427
+ if ((await engine.files.read(asset.artifactId, ".analysis.gpt4otrans.json")).ok) {
1428
+ return null;
1429
+ }
1430
+ const audioFile = findAudioFile(files, asset.kind);
1431
+ if (!audioFile)
1432
+ return null;
1433
+ const videoFile = files.find((file) => file.name === "original.mp4")
1434
+ ?? findVideoFile(files);
1435
+ if (!videoFile)
1436
+ return null;
1437
+ const revision = videoFile.objectHash;
1438
+ const existing = engine.jobs.queue.list({
1439
+ type: "transcribe_video_audio",
1440
+ artifactId: asset.artifactId,
1441
+ });
1442
+ if (existing.some((job) => job.payload.mediaRevision === revision))
1443
+ return null;
1444
+ return {
1445
+ assetId: asset.artifactId,
1446
+ assetDir: manifest.path,
1447
+ videoFilename: videoFile.name,
1448
+ revision,
1449
+ };
1450
+ }));
1451
+ return candidates.filter((c) => c !== null);
1452
+ }