@gentbajko/slopify 2.5.0 → 3.0.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 (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,6 +1,7 @@
1
1
  import { stageKinds } from "../../kernel/pipeline.js";
2
2
  import { sourceOf } from "../admission/model.js";
3
- import { allowedSources, edgeSilenceSecondsProblem, imageSecondsProblem, normaliseDraft, silenceGapSecondsMax, zoomPercentProblem, } from "../admission/rules.js";
3
+ import { allowedSources, edgeSilenceSecondsProblem, imageSecondsProblem, normaliseDraft, silenceGapSecondsMax, voicesFields, zoomPercentProblem, } from "../admission/rules.js";
4
+ import { shortModeFields } from "../admission/short-mode.js";
4
5
  import { render } from "../admission/substitute.js";
5
6
  import { effectiveClips, pickedShortsOf, rangeProblem } from "../shorts/clips.js";
6
7
  import { buildRecipes } from "./recipe-build.js";
@@ -44,6 +45,7 @@ export function planRevision(base, edit, prepared = { outputs: [], pieces: [] },
44
45
  const content = normalizeImages(normalizeArticleIntent(base, edit));
45
46
  const fields = [
46
47
  ...validateRevisionEdit(edit.config, content),
48
+ ...shortModeFields(edit.config),
47
49
  ...validateCues(content),
48
50
  ...validateRecipeInputs(edit.config, content),
49
51
  ...shortRangeFields(base, edit.config, content),
@@ -234,6 +236,7 @@ export function validateRevisionEdit(config, content) {
234
236
  config.subtitles !== undefined &&
235
237
  config.subtitles.mode !== "off")
236
238
  fields.push({ field: "subtitles.mode", message: "Subtitles need narration audio." });
239
+ fields.push(...voicesFields(config));
237
240
  return fields;
238
241
  }
239
242
  function validateCues(content) {
@@ -4,8 +4,10 @@ import { usesShorts } from "../admission/rules.js";
4
4
  import { effectiveClips, pickedShortsOf } from "../shorts/clips.js";
5
5
  import { defaultShortsImagePrompt, musicVolumeOf, shortImageCount, shortsSpeedOf, } from "../shorts/model.js";
6
6
  import { imagePromptMessages } from "../shorts/prompts.js";
7
+ import { castFor } from "./recipe-cast.js";
8
+ import { noMaster } from "./recipe-loudness.js";
7
9
  import { recipe, resourceIdentity, selectedReference, } from "./recipe-model.js";
8
- import { imageChoice } from "./recipe-reference.js";
10
+ import { castField, imageChoice } from "./recipe-reference.js";
9
11
  import { llmInput, renderedPrompt } from "./recipe-text.js";
10
12
  // The Shorts step unfolds as its answers land, the way research's chapters unfold from the
11
13
  // plan. `shorts:pick` reads the word timing, so it waits for `subtitles:timing` and runs
@@ -22,7 +24,11 @@ export const shortsPickKey = "shorts:pick";
22
24
  export const shortsFutureKey = "shorts:future";
23
25
  export function shortsRecipes(context, exports,
24
26
  // The establishing image the shorts' images are drawn from, when it is on.
25
- reference) {
27
+ reference,
28
+ // "Show tables and figures on screen": the cards a short shows where it describes one.
29
+ cards = [],
30
+ // Level the volume: the levelled narration and the master (`recipe-loudness.ts`).
31
+ master = noMaster) {
26
32
  const { config } = context;
27
33
  const shorts = config.shorts;
28
34
  const timing = exports.find((value) => value.key === "subtitles:timing");
@@ -44,6 +50,8 @@ reference) {
44
50
  shorts.minSeconds,
45
51
  shorts.maxSeconds,
46
52
  config.title,
53
+ // The project language, only when it is not English, so English keeps its fingerprint.
54
+ ...(config.language === undefined || config.language === "en" ? [] : [config.language]),
47
55
  ],
48
56
  }, [timing.key],
49
57
  // A paid LLM call: readiness, the charge warning and the estimate treat it like one.
@@ -105,6 +113,7 @@ reference) {
105
113
  prompt,
106
114
  aspect: "9:16",
107
115
  ...(reference === undefined ? {} : { reference: reference.input }),
116
+ ...castField(castFor(config, prompt)),
108
117
  }, [prompts.key, ...(reference === undefined ? [] : [reference.key])], { token }));
109
118
  recipes.push(...stills);
110
119
  const extras = renderExtras(context, clip);
@@ -125,8 +134,19 @@ reference) {
125
134
  // Only when one of the later settings is in use, so a short rendered before
126
135
  // they existed keeps its fingerprint.
127
136
  ...(extras === undefined ? [] : [extras]),
137
+ // Only with cards, so a short rendered before them keeps its fingerprint.
138
+ ...(cards.length === 0
139
+ ? []
140
+ : [["figure-cards-v1", cards.map((card) => resourceIdentity(context, card))]]),
141
+ // Only while the volume is levelled, so every short without it keeps its fingerprint.
142
+ ...master.values,
128
143
  ],
129
- }, [pick.key, ...stills.map((still) => still.key)], { token }));
144
+ }, [
145
+ pick.key,
146
+ ...stills.map((still) => still.key),
147
+ ...cards.map((card) => card.key),
148
+ ...master.keys,
149
+ ], { token }));
130
150
  }
131
151
  return recipes;
132
152
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { withLanguage } from "../../kernel/ports/languages.js";
2
3
  import { documentIndex } from "../../kernel/ports/llm-documents.js";
3
4
  import { fingerprint } from "../../kernel/runner/work.js";
4
5
  import { usesPronunciationGlossary } from "../admission/rules.js";
@@ -7,12 +8,17 @@ import { articleMessages, continuationMessages } from "../article/continuation.j
7
8
  import { plainText } from "../article/plain.js";
8
9
  import { segmentMessages } from "../article/segments.js";
9
10
  import { splitEndMatter } from "../article/split.js";
11
+ import { withEarlierEpisodes } from "../episodes/related.js";
10
12
  import { parsePronunciationGlossary } from "../narration/pronunciation.js";
11
13
  import { withSharedGlossary } from "../narration/shared-glossary.js";
12
14
  import { researchDocuments } from "../research/documents.js";
13
15
  import { plannerMessages, subAgentMessages } from "../research/planner.js";
14
16
  import { synthesisMessages } from "../research/synthesis.js";
15
17
  import { thumbnailMessages } from "../thumbnail/by-llm.js";
18
+ import { attributionMessages } from "../voices/attribution.js";
19
+ import { usesVoices } from "../voices/model.js";
20
+ import { scriptMessages } from "../voices/script.js";
21
+ import { describedText, describeFuture, describing } from "./recipe-describe.js";
16
22
  import { recipe, selectedReference, } from "./recipe-model.js";
17
23
  export function renderedPrompt(context, key) {
18
24
  const raw = context.content.promptTemplates[key];
@@ -41,7 +47,12 @@ export function llmInput(context, messages, webSearch = false, documents) {
41
47
  export function selectedText(context, key, field) {
42
48
  const piece = context.manifest.pieces.find((row) => row.key === key &&
43
49
  selectedReference(row) &&
44
- row.stageKind === (key.startsWith("entry:") ? "article" : "thumbnail") &&
50
+ row.stageKind ===
51
+ (key.startsWith("entry:") || key.startsWith("script:")
52
+ ? "article"
53
+ : key.startsWith("narration:describe:")
54
+ ? "audio"
55
+ : "thumbnail") &&
45
56
  row.piece.state === "done");
46
57
  if (piece?.piece.payload === null || piece === undefined)
47
58
  return null;
@@ -81,9 +92,7 @@ export function textRecipes(context) {
81
92
  });
82
93
  recipes.push(research);
83
94
  }
84
- const articleMarkdown = config.sources.article === "provide" || content.articleEdited === true
85
- ? (content.articleMarkdown ?? config.provided.article ?? "")
86
- : resolved.articleMarkdown;
95
+ const articleMarkdown = articleMarkdownOf(context);
87
96
  const notes = config.sources.research === "off"
88
97
  ? null
89
98
  : config.sources.research === "provide"
@@ -95,10 +104,26 @@ export function textRecipes(context) {
95
104
  { id: "editorial-notes", title: "Editorial notes", content: notes },
96
105
  ]
97
106
  : [];
98
- const messages = articleMessages({
99
- articlePrompt: brief.articlePrompt,
100
- ...(documents.length ? { notes: documentIndex(documents) } : notes === null ? {} : { notes }),
101
- });
107
+ const voices = usesVoices(config) ? config.voices : undefined;
108
+ const writesScript = voices?.source === "script";
109
+ const articleNotes = documents.length ? documentIndex(documents) : (notes ?? undefined);
110
+ // A script run writes speaker turns in place of the article, from the same prompt and notes.
111
+ // The related earlier episodes go under the prompt only when the run carries them, so a
112
+ // project made without them keeps its fingerprint. Either is written in the project's
113
+ // language (`withLanguage`; English adds nothing).
114
+ const writtenPrompt = withEarlierEpisodes(brief.articlePrompt, config.earlierEpisodes);
115
+ const messages = withLanguage(voices !== undefined && writesScript
116
+ ? scriptMessages(voices.format, voices.speakers, writtenPrompt, articleNotes)
117
+ : articleMessages({
118
+ articlePrompt: writtenPrompt,
119
+ ...(articleNotes === undefined ? {} : { notes: articleNotes }),
120
+ }), config.language);
121
+ const scriptCheck = voices === undefined
122
+ ? undefined
123
+ : {
124
+ speakers: voices.speakers.map((speaker) => ({ id: speaker.id, name: speaker.name })),
125
+ attribute: !writesScript,
126
+ };
102
127
  const article = config.sources.article === "provide" || content.articleEdited === true
103
128
  ? recipe(context, "article:body", "article", {
104
129
  kind: "local",
@@ -113,7 +138,10 @@ export function textRecipes(context) {
113
138
  operation: "article",
114
139
  template: [llmInputFingerprint(context, messages), research?.fingerprint ?? null],
115
140
  }
116
- : llmInput(context, messages, false, documents), research === undefined ? [] : [research.key]);
141
+ : {
142
+ ...llmInput(context, messages, false, documents),
143
+ ...(writesScript && scriptCheck !== undefined ? { script: scriptCheck } : {}),
144
+ }, research === undefined ? [] : [research.key]);
117
145
  recipes.push(article);
118
146
  if (resolved.articleContinuation !== undefined &&
119
147
  config.sources.article === "generate" &&
@@ -121,11 +149,50 @@ export function textRecipes(context) {
121
149
  recipes.push(recipe(context, "article:continuation", "article", llmInput(context, continuationMessages(messages, resolved.articleContinuation), false, documents), [article.key]));
122
150
  const endMatter = articleMarkdown === null ? null : splitEndMatter(articleMarkdown);
123
151
  const articleText = endMatter === null ? null : plainText(endMatter.body);
124
- const glossary = !usesPronunciationGlossary(config)
125
- ? { ok: true, entries: [] }
126
- : endMatter === null
127
- ? null
128
- : withShared(parsePronunciationGlossary(endMatter.glossary), config);
152
+ let script;
153
+ if (voices !== undefined && scriptCheck !== undefined) {
154
+ if (writesScript)
155
+ script = { text: endMatter?.body ?? null, dependsOn: [article.key] };
156
+ else {
157
+ // An audiobook from a text: the text model hands the article's passages to speakers.
158
+ const attribute = recipe(context, "script:attribute", "article", endMatter === null
159
+ ? {
160
+ kind: "deferred",
161
+ version: 1,
162
+ operation: "script-attribution",
163
+ template: [
164
+ article.fingerprint,
165
+ llmInputFingerprint(context, withLanguage(attributionMessages("", voices.speakers), config.language)),
166
+ ],
167
+ }
168
+ : {
169
+ ...llmInput(context, withLanguage(attributionMessages(endMatter.body, voices.speakers), config.language)),
170
+ script: scriptCheck,
171
+ }, [article.key]);
172
+ recipes.push(attribute);
173
+ script = {
174
+ text: endMatter === null ? null : matchingText(context, attribute, "text"),
175
+ dependsOn: [attribute.key],
176
+ fingerprint: attribute.fingerprint,
177
+ };
178
+ }
179
+ }
180
+ let narrationText = articleText;
181
+ const descriptions = [];
182
+ const cards = [];
183
+ if (describing(context) && voices === undefined) {
184
+ if (endMatter === null) {
185
+ descriptions.push(describeFuture(context, article));
186
+ narrationText = null;
187
+ }
188
+ else {
189
+ const described = describedText(context, endMatter.body, [article.key]);
190
+ descriptions.push(...described.recipes);
191
+ cards.push(...described.cards);
192
+ narrationText = described.spoken;
193
+ }
194
+ }
195
+ const glossary = glossaryOf(context, endMatter);
129
196
  const entries = {};
130
197
  for (const category of ["intro", "outro"]) {
131
198
  const choice = config[category];
@@ -141,11 +208,11 @@ export function textRecipes(context) {
141
208
  version: 1,
142
209
  operation: key,
143
210
  template: [
144
- llmInputFingerprint(context, segmentMessages(prompt, config, "")),
211
+ llmInputFingerprint(context, withLanguage(segmentMessages(prompt, config, ""), config.language)),
145
212
  article.fingerprint,
146
213
  ],
147
214
  }
148
- : llmInput(context, segmentMessages(prompt, config, articleText));
215
+ : llmInput(context, withLanguage(segmentMessages(prompt, config, articleText), config.language));
149
216
  const value = recipe(context, key, "article", input, choice.mode === "llm" ? [article.key] : []);
150
217
  recipes.push(value);
151
218
  entries[category] = {
@@ -171,7 +238,17 @@ export function textRecipes(context) {
171
238
  }
172
239
  : llmInput(context, messages), [article.key]));
173
240
  }
174
- return { recipes, articleText, glossary, article, entries };
241
+ return {
242
+ recipes,
243
+ articleText,
244
+ narrationText,
245
+ descriptions,
246
+ cards,
247
+ glossary,
248
+ article,
249
+ entries,
250
+ ...(script === undefined ? {} : { script }),
251
+ };
175
252
  }
176
253
  function llmInputFingerprint(context, messages) {
177
254
  return fingerprint(JSON.parse(JSON.stringify(llmInput(context, messages))));
@@ -186,10 +263,28 @@ export function matchingText(context, value, field) {
186
263
  return null;
187
264
  return selectedText(context, value.key, field);
188
265
  }
266
+ function articleMarkdownOf(context) {
267
+ const { config, content, resolved } = context;
268
+ return config.sources.article === "provide" || content.articleEdited === true
269
+ ? (content.articleMarkdown ?? config.provided.article ?? "")
270
+ : resolved.articleMarkdown;
271
+ }
272
+ function glossaryOf(context, endMatter) {
273
+ if (!usesPronunciationGlossary(context.config))
274
+ return { ok: true, entries: [] };
275
+ if (endMatter === null)
276
+ return null;
277
+ return withShared(parsePronunciationGlossary(endMatter.glossary, context.config.language), context.config);
278
+ }
279
+ // The narration glossary this revision uses, or null while its article is still unwritten.
280
+ export function narrationGlossary(context) {
281
+ const markdown = articleMarkdownOf(context);
282
+ return glossaryOf(context, markdown === null ? null : splitEndMatter(markdown));
283
+ }
189
284
  // The project's own glossary, then the other projects' pronunciations it copied, when it
190
- // shares them. An own glossary that doesn't parse still refuses, as before.
285
+ // shares them. Rows the own glossary had to skip are still reported.
191
286
  function withShared(own, config) {
192
287
  if (!own.ok || config.audio?.shareGlossary !== true)
193
288
  return own;
194
- return { ok: true, entries: withSharedGlossary(own.entries, config.sharedGlossary) };
289
+ return { ...own, entries: withSharedGlossary(own.entries, config.sharedGlossary) };
195
290
  }
@@ -1,5 +1,9 @@
1
1
  import { narrationPreparationFields, shortsFields, usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, videoEditFields, youtubeDescriptionFields, } from "../admission/rules.js";
2
2
  import { detectSlots, render } from "../admission/substitute.js";
3
+ import { imagesPerVideoMax } from "../images/scale.js";
4
+ import { loudnessFields } from "../loudness/model.js";
5
+ import { pauseFields } from "../narration/pauses-model.js";
6
+ import { reviewFields } from "../reviews/rules.js";
3
7
  import { usesAnimation } from "../video/edit-settings.js";
4
8
  export function validateRecipeInputs(config, content) {
5
9
  const fields = [
@@ -7,6 +11,9 @@ export function validateRecipeInputs(config, content) {
7
11
  ...youtubeDescriptionFields(config),
8
12
  ...shortsFields(config),
9
13
  ...videoEditFields(config),
14
+ ...reviewFields(config),
15
+ ...loudnessFields(config.loudness),
16
+ ...pauseFields(config),
10
17
  ];
11
18
  const llm = (config.sources.research === "generate" && config.sources.article !== "provide") ||
12
19
  (config.sources.article === "generate" && !content.articleEdited) ||
@@ -33,8 +40,13 @@ export function validateRecipeInputs(config, content) {
33
40
  usesAnimation(config)) &&
34
41
  (!config.images?.provider.trim() || !config.images.model.trim()))
35
42
  fields.push({ field: "images", message: "Pick an image provider and model." });
36
- if (content.imageOrder.length > 60)
37
- fields.push({ field: "content.imageOrder", message: "Keep at most 60 images." });
43
+ // 60, or more for a project that scales its images with the narration (`images/scale.ts`).
44
+ const imagesMax = imagesPerVideoMax(config);
45
+ if (content.imageOrder.length > imagesMax)
46
+ fields.push({
47
+ field: "content.imageOrder",
48
+ message: `Keep at most ${String(imagesMax)} images.`,
49
+ });
38
50
  const chunking = config.chunking;
39
51
  const size = chunking?.mode === "words"
40
52
  ? chunking.words
@@ -1,12 +1,21 @@
1
+ import { thumbnailCountOf, thumbnailKey } from "../admission/model.js";
2
+ import { usesShortMode } from "../admission/short-mode.js";
1
3
  import { render } from "../admission/substitute.js";
4
+ import { usesAmbientBed } from "../video/ambient-bed.js";
5
+ import { castFor } from "./recipe-cast.js";
6
+ import { noMaster } from "./recipe-loudness.js";
2
7
  import { recipe, resourceIdentity, } from "./recipe-model.js";
3
- import { imageChoice } from "./recipe-reference.js";
8
+ import { castField, imageChoice } from "./recipe-reference.js";
4
9
  import { matchingText, renderedPrompt } from "./recipe-text.js";
5
10
  export function visualRecipes(config, content, audioFingerprint, captionFingerprint,
6
11
  // What the Video stage's edit settings add (`recipe-edit.ts`), from the images planned here.
7
12
  edit,
8
13
  // The establishing image the generated images are drawn from, when it is on.
9
- reference) {
14
+ reference,
15
+ // The word timing's identity, which a short's captions are drawn from.
16
+ timing = null,
17
+ // Level the volume: the levelled narration and the master (`recipe-loudness.ts`).
18
+ master = noMaster) {
10
19
  const recipes = [];
11
20
  const imageKeys = config.sources.images === "off" ? [] : content.imageOrder;
12
21
  for (const key of imageKeys) {
@@ -26,10 +35,19 @@ reference) {
26
35
  aspect: config.format,
27
36
  prompt: prompt ?? "",
28
37
  ...(reference === undefined ? {} : { reference: reference.input }),
38
+ ...castField(castFor(config, prompt)),
29
39
  }, image.source === "provide" || reference === undefined ? [] : [reference.key], { unresolved: image.source === "provide" ? image.assetId === null : !prompt?.trim() }));
30
40
  }
31
41
  if (config.sources.video !== "off") {
32
- const edited = edit?.([...recipes]) ?? { recipes: [], values: [], dependsOn: [] };
42
+ const short = usesShortMode(config);
43
+ // A short is rendered by the Shorts renderer, which has no cuts, transitions or Look.
44
+ const edited = (short ? undefined : edit?.([...recipes])) ?? {
45
+ recipes: [],
46
+ values: [],
47
+ dependsOn: [],
48
+ };
49
+ // A short (rendered by the Shorts renderer) never has the bed.
50
+ const bed = short ? undefined : ambientBedValues(config, content);
33
51
  const audioKeys = config.sources.audio === "off"
34
52
  ? []
35
53
  : [
@@ -64,18 +82,48 @@ reference) {
64
82
  // Only what the edit settings change; nothing at all for today's slideshow, so
65
83
  // its fingerprint is the one it always had.
66
84
  ...(edited.values.length === 0 ? [] : [["video-edit", ...edited.values]]),
85
+ // Only for a short, so every long video keeps its fingerprint: the word-by-word
86
+ // captions are drawn from the word timing in the caption font, under the title.
87
+ ...(short
88
+ ? [["short-v1", config.subtitles?.fontId ?? "default", config.title, timing]]
89
+ : []),
90
+ // Only while the video has an ambient bed, so every video without one keeps the
91
+ // fingerprint it always had.
92
+ ...(bed === undefined ? [] : [bed]),
93
+ // Only while the volume is levelled, so every video without it keeps its fingerprint.
94
+ ...master.values,
67
95
  ],
68
96
  }, [
69
97
  ...recipes.map((value) => value.key),
70
98
  ...audioKeys,
99
+ ...master.keys,
71
100
  ...(config.subtitles?.mode === "burn-in" ? ["subtitles:files"] : []),
101
+ ...(short ? ["subtitles:timing"] : []),
72
102
  ...edited.dependsOn,
73
- ], { unresolved: imageKeys.length === 0 }));
103
+ ], {
104
+ unresolved: imageKeys.length === 0 ||
105
+ (config.ambientBed?.source === "upload" && content.ambientBed === undefined),
106
+ }));
74
107
  // After the export, so the images' recipes above stay the only ones its first values list.
75
108
  recipes.splice(recipes.length - 1, 0, ...edited.recipes);
76
109
  }
77
110
  return recipes;
78
111
  }
112
+ // What the ambient bed adds to the render's fingerprint: its settings and, for the user's own
113
+ // file, the project asset it plays. Undefined without a bed.
114
+ export function ambientBedValues(config, content) {
115
+ const bed = config.ambientBed;
116
+ if (bed === undefined || !usesAmbientBed(config))
117
+ return undefined;
118
+ return [
119
+ "ambient-bed-v1",
120
+ bed.source,
121
+ bed.levelDb,
122
+ bed.fadeInSeconds,
123
+ bed.tailSeconds,
124
+ bed.source === "upload" ? (content.ambientBed ?? null) : null,
125
+ ];
126
+ }
79
127
  export function thumbnailRecipes(context, textRecipes,
80
128
  // The establishing image, when it is on and the thumbnail is drawn from it too.
81
129
  reference) {
@@ -95,35 +143,50 @@ reference) {
95
143
  const prompt = config.sources.thumbnail === "prompt_by_llm" && promptRecipe !== undefined
96
144
  ? matchingText(context, promptRecipe, "prompt")
97
145
  : renderedPrompt(context, "thumbnailPrompt");
98
- return [
99
- recipe(context, "thumbnail:image", "thumbnail", prompt === null
100
- ? {
101
- kind: "deferred",
102
- version: 1,
103
- operation: "thumbnail-image",
104
- template: [
105
- promptRecipe?.fingerprint ?? null,
106
- config.images?.provider ?? null,
107
- config.images?.model ?? null,
108
- config.format,
109
- // Only what is in use, so a thumbnail made before these existed keeps its
110
- // fingerprint.
111
- ...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
112
- ...(reference === undefined ? [] : [reference.input.fingerprint]),
113
- ],
114
- }
115
- : {
116
- kind: "image",
117
- version: 1,
118
- ...imageChoice(config),
119
- aspect: config.format,
120
- prompt,
121
- ...(reference === undefined ? {} : { reference: reference.input }),
122
- }, [
123
- ...(promptRecipe === undefined ? [] : [promptRecipe.key]),
124
- ...(reference === undefined ? [] : [reference.key]),
125
- ]),
126
- ];
146
+ const variants = Array.from({ length: thumbnailCountOf(config) }, (_, index) => index + 1);
147
+ return variants.map((variant) => recipe(context, thumbnailKey(variant), "thumbnail", prompt === null
148
+ ? {
149
+ kind: "deferred",
150
+ version: 1,
151
+ operation: "thumbnail-image",
152
+ template: [
153
+ promptRecipe?.fingerprint ?? null,
154
+ config.images?.provider ?? null,
155
+ config.images?.model ?? null,
156
+ config.format,
157
+ // Only what is in use, so a thumbnail made before these existed keeps its
158
+ // fingerprint.
159
+ ...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
160
+ ...(reference === undefined ? [] : [reference.input.fingerprint]),
161
+ ...(variant === 1 ? [] : [["thumbnail-variant", variant]]),
162
+ ],
163
+ }
164
+ : {
165
+ kind: "image",
166
+ version: 1,
167
+ ...imageChoice(config),
168
+ aspect: config.format,
169
+ prompt: thumbnailVariantPrompt(prompt, variant),
170
+ ...(reference === undefined ? {} : { reference: reference.input }),
171
+ // The thumbnail stands for the whole video, like the establishing image.
172
+ ...castField(castFor(config, config.title, prompt)),
173
+ }, [
174
+ ...(promptRecipe === undefined ? [] : [promptRecipe.key]),
175
+ ...(reference === undefined ? [] : [reference.key]),
176
+ ]));
177
+ }
178
+ // The second and third thumbnails are the same prompt asked for another composition, so the
179
+ // three test one idea against itself rather than three ideas. The first is the prompt as it
180
+ // is, which keeps the thumbnail every project already has.
181
+ const variantFraming = {
182
+ 2: "a tight close-up where the main subject fills most of the frame, seen from a different angle",
183
+ 3: "a wider shot that shows the main subject in its setting, placed off-centre with room around it",
184
+ };
185
+ export function thumbnailVariantPrompt(prompt, variant) {
186
+ const framing = variantFraming[variant];
187
+ if (framing === undefined)
188
+ return prompt;
189
+ return `${prompt}\n\nThis is thumbnail ${String(variant)} of 3 for a YouTube thumbnail test. Keep the subject, style, colours and any text asked for above, but use a clearly different composition: ${framing}.`;
127
190
  }
128
191
  export function visualAssets(context, recipes) {
129
192
  const images = recipes.filter((value) => value.stage === "images");