@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
@@ -0,0 +1,164 @@
1
+ import { renameSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { runFfmpeg } from "../video/ffmpeg.js";
4
+ // ceiling: below -70 LUFS (the filter's absolute gate) or under 0.4 s (one measuring block)
5
+ // there is nothing to measure: a piece that quiet or short is left as it is.
6
+ const silentBelow = -70;
7
+ const measuredJson = z.object({
8
+ input_i: z.string(),
9
+ input_tp: z.string(),
10
+ input_lra: z.string(),
11
+ input_thresh: z.string(),
12
+ target_offset: z.string(),
13
+ });
14
+ function measureFilter(goal) {
15
+ return `loudnorm=I=${goal.lufs.toFixed(1)}:TP=${goal.truePeak.toFixed(1)}:LRA=11:print_format=json`;
16
+ }
17
+ // The first pass: decode, measure, write nothing. `input` is ffmpeg input arguments, so a
18
+ // filter graph (the video's mix) can be measured as well as a file.
19
+ export function measureArgs(input, goal, chain = "") {
20
+ return [
21
+ "-hide_banner",
22
+ "-nostdin",
23
+ // loudnorm prints its JSON at the info level.
24
+ "-loglevel",
25
+ "info",
26
+ "-nostats",
27
+ ...input,
28
+ "-af",
29
+ `${chain === "" ? "" : `${chain},`}${measureFilter(goal)}`,
30
+ "-f",
31
+ "null",
32
+ "-",
33
+ ];
34
+ }
35
+ // The second pass: the gain that lands the file on the level, the peak limiter where the gain
36
+ // would push the peaks over the ceiling, and the file's sample rate. Unity for a file there was
37
+ // nothing to measure in.
38
+ export function gainFilter(goal, measured, sampleRate) {
39
+ const resample = `aresample=${String(sampleRate)}`;
40
+ if (measured === undefined || !measurable(measured))
41
+ return resample;
42
+ const gain = goal.lufs - measured.integrated;
43
+ const limit = Number.isFinite(measured.truePeak) && measured.truePeak + gain > goal.truePeak
44
+ ? `,aresample=192000,alimiter=limit=${(10 ** (goal.truePeak / 20)).toFixed(4)}:attack=5:release=50:level=false`
45
+ : "";
46
+ return `volume=${gain.toFixed(2)}dB${limit},${resample}`;
47
+ }
48
+ export function measurable(measured) {
49
+ return Number.isFinite(measured.integrated) && measured.integrated > silentBelow;
50
+ }
51
+ // The last JSON block loudnorm printed. Its numbers are strings, and "-inf" for silence.
52
+ export function parseMeasured(stderr) {
53
+ const start = stderr.lastIndexOf("{");
54
+ const end = stderr.lastIndexOf("}");
55
+ if (start < 0 || end < start)
56
+ return undefined;
57
+ let raw;
58
+ try {
59
+ raw = JSON.parse(stderr.slice(start, end + 1));
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ const parsed = measuredJson.safeParse(raw);
65
+ if (!parsed.success)
66
+ return undefined;
67
+ const number = (text) => {
68
+ const value = Number(text);
69
+ return Number.isNaN(value) ? Number.NEGATIVE_INFINITY : value;
70
+ };
71
+ return {
72
+ integrated: number(parsed.data.input_i),
73
+ truePeak: number(parsed.data.input_tp),
74
+ range: number(parsed.data.input_lra),
75
+ threshold: number(parsed.data.input_thresh),
76
+ offset: number(parsed.data.target_offset),
77
+ };
78
+ }
79
+ // Measure `input` (ffmpeg input arguments, `chain` an optional filter before the measurement).
80
+ export async function measure(run, input, goal, chain = "") {
81
+ let stderr = "";
82
+ await runFfmpeg({
83
+ bin: run.bin,
84
+ args: measureArgs(input, goal, chain),
85
+ signal: run.signal,
86
+ log: run.log,
87
+ onProgress: () => { },
88
+ onStderr: (text) => {
89
+ stderr += text;
90
+ },
91
+ });
92
+ const measured = parseMeasured(stderr);
93
+ if (measured === undefined)
94
+ throw new Error("Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in Edit project → Pauses and volume, then use Download diagnostics in Settings and report it.");
95
+ return measured;
96
+ }
97
+ export async function measureFile(run, path, goal) {
98
+ return measure(run, ["-i", path], goal);
99
+ }
100
+ // One file measured and written again at `goal`, as WAV at `sampleRate` with `channels`
101
+ // channels (32-bit float, so nothing clips between here and the encode). Returns what the first
102
+ // pass measured.
103
+ export async function normalizeFile(run, input, output, goal, format) {
104
+ // The channels are set before anything is measured: a stereo file mixed down to mono is
105
+ // louder per channel (the mix keeps its loudness, so each sample is about 3 dB higher), and
106
+ // the gain and the peak ceiling have to hold for what is written.
107
+ const channels = `aformat=channel_layouts=${format.channels === 1 ? "mono" : "stereo"}`;
108
+ const measured = await measure(run, ["-i", input], goal, channels);
109
+ await runFfmpeg({
110
+ bin: run.bin,
111
+ args: [
112
+ "-hide_banner",
113
+ "-nostdin",
114
+ "-loglevel",
115
+ "error",
116
+ "-nostats",
117
+ "-y",
118
+ "-i",
119
+ input,
120
+ "-map",
121
+ "0:a:0",
122
+ "-af",
123
+ `${channels},${gainFilter(goal, measured, format.sampleRate)}`,
124
+ "-c:a",
125
+ "pcm_f32le",
126
+ "-f",
127
+ "wav",
128
+ output,
129
+ ],
130
+ signal: run.signal,
131
+ log: run.log,
132
+ onProgress: () => { },
133
+ });
134
+ return measured;
135
+ }
136
+ // ceiling: within a fifth of a LU of the level no one hears the difference.
137
+ const correctAbove = 0.2;
138
+ // One file brought to `goal` (`normalizeFile`), measured again, and corrected once when the
139
+ // limiter left it more than a fifth of a LU under the level. Returns the first measurement and
140
+ // the last.
141
+ export async function levelFile(run, input, output, goal, format) {
142
+ const before = await normalizeFile(run, input, output, goal, format);
143
+ let after = await measureFile(run, output, goal);
144
+ if (measurable(after) && Math.abs(after.integrated - goal.lufs) > correctAbove) {
145
+ const corrected = `${output}.corrected.wav`;
146
+ await normalizeFile(run, output, corrected, goal, format);
147
+ renameSync(corrected, output);
148
+ after = await measureFile(run, output, goal);
149
+ }
150
+ return { before, after };
151
+ }
152
+ // A finished file's sound brought to its master target.
153
+ export async function masterFile(run, input, output, goal, format) {
154
+ await levelFile(run, input, output, goal, format);
155
+ }
156
+ // What a finished file measures, for its output (`MasterReport`).
157
+ export async function masterReport(run, file, goal) {
158
+ const check = await measureFile(run, file, goal);
159
+ return {
160
+ target: goal.lufs,
161
+ integrated: Math.round(check.integrated * 10) / 10,
162
+ truePeak: Math.round(check.truePeak * 10) / 10,
163
+ };
164
+ }
@@ -0,0 +1,153 @@
1
+ // Loudness normalization ("Level the volume"). Text-to-speech comes back at a different
2
+ // loudness per voice and even per request, so a joined narration jumps. With the setting on:
3
+ // - every narration piece (a chunk, an intro or outro part, a speaker's turn) is measured and
4
+ // brought to one common loudness before the pieces are joined (`loudnorm.ts`), in a step of
5
+ // its own beside the plain join (`recipe-audio.ts`), so the word timing, which reads the
6
+ // plain join, and everything written from it are never made again for a volume change;
7
+ // - every finished file is mastered to a target: the long video and the shorts to the video
8
+ // target, the audio files (the audio-only WAV, an audiobook's MP3 and M4B) to their own.
9
+ // The ambient bed and the shorts' music are mixed before the master and ducked by the levelled
10
+ // voice, so they keep their place under it.
11
+ //
12
+ // Browser-safe: Play, Edit project and Settings convert the same numbers.
13
+ //
14
+ // A run without the setting has no `loudness` at all, which is what every project saved before
15
+ // it was, and every recipe leaves it out of its fingerprint then.
16
+ // YouTube and Spotify play everything back at about -14 LUFS, so a video mastered there is
17
+ // neither turned down nor left quiet. Audiobook shops ask for about -18 to -20 LUFS with more
18
+ // headroom, so the listening files sit lower.
19
+ export const recommendedVideoLufs = -14;
20
+ export const recommendedAudioFilesLufs = -18;
21
+ export const defaultLoudness = {
22
+ enabled: true,
23
+ videoLufs: recommendedVideoLufs,
24
+ audioFilesLufs: recommendedAudioFilesLufs,
25
+ };
26
+ // The peak ceilings, in dBTP (true peak, between the samples). The video's leaves room for the
27
+ // lossy AAC encode; the audio files' is what audiobook shops ask for.
28
+ export const videoTruePeak = -1.5;
29
+ export const audioFilesTruePeak = -3;
30
+ // The master aims this far under the ceiling: a lossy encode (the video's AAC, the MP3 and
31
+ // M4B) lifts the peaks by up to about 0.7 dB at the bitrates Slopify writes, and what is
32
+ // measured is the finished file.
33
+ export const encodeHeadroom = 1;
34
+ // The common level every narration piece is brought to before the join. Fixed, so changing the
35
+ // Volume only masters the finished files again and never re-joins the narration. ceiling: a
36
+ // voice's peaks run 14 to 18 dB over its loudness, so -20 LUFS leaves room for nearly all of
37
+ // them under -2 dBTP; the master then lifts the whole to the target.
38
+ export const pieceLufs = -20;
39
+ export const pieceTruePeak = -2;
40
+ // The Volume control is in dB from the recommended level: 0 dB is the recommendation. Below
41
+ // -10 dB a video sounds broken next to others; above +4 dB the peak ceiling squashes the voice.
42
+ export const loudnessDbMin = -10;
43
+ export const loudnessDbMax = 4;
44
+ export const loudnessDbStep = 0.5;
45
+ // The master a run's file of this kind gets, or undefined while the setting is off.
46
+ export function masterGoal(config, target) {
47
+ const settings = config.loudness;
48
+ if (settings === undefined || !usesLoudness(config))
49
+ return undefined;
50
+ return {
51
+ lufs: target === "video" ? settings.videoLufs : settings.audioFilesLufs,
52
+ truePeak: truePeakOf(target) - encodeHeadroom,
53
+ };
54
+ }
55
+ export function recommendedLufs(target) {
56
+ return target === "video" ? recommendedVideoLufs : recommendedAudioFilesLufs;
57
+ }
58
+ export function truePeakOf(target) {
59
+ return target === "video" ? videoTruePeak : audioFilesTruePeak;
60
+ }
61
+ // dB from the recommended level, and back. Stored is the LUFS; the dB is what is shown.
62
+ export function dbOfLufs(lufs, target) {
63
+ return round(lufs - recommendedLufs(target), 2);
64
+ }
65
+ export function lufsOfDb(db, target) {
66
+ return round(recommendedLufs(target) + db, 2);
67
+ }
68
+ // Loudness as a share of the recommended level, the way a volume knob reads: -6 dB is about
69
+ // half as loud, +6 dB about twice. An amplitude ratio, so it matches what the gain does.
70
+ export function percentOfDb(db) {
71
+ return Math.round(10 ** (db / 20) * 100);
72
+ }
73
+ export function dbOfPercent(percent) {
74
+ return round(20 * Math.log10(percent / 100), 2);
75
+ }
76
+ // Typed dB snapped to the control's step and range; undefined for text that is not a number.
77
+ export function snapDb(db) {
78
+ if (!Number.isFinite(db))
79
+ return undefined;
80
+ const snapped = Math.round(db / loudnessDbStep) * loudnessDbStep;
81
+ return Math.min(loudnessDbMax, Math.max(loudnessDbMin, snapped));
82
+ }
83
+ export function loudnessProblem(lufs, target) {
84
+ const db = lufs - recommendedLufs(target);
85
+ return Number.isFinite(db) &&
86
+ Number.isInteger(round(db / loudnessDbStep, 6)) &&
87
+ db >= loudnessDbMin - 1e-9 &&
88
+ db <= loudnessDbMax + 1e-9
89
+ ? undefined
90
+ : `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (Play → Outputs → Export, Edit project → Pauses and volume, or Settings → General).`;
91
+ }
92
+ export function loudnessFields(settings) {
93
+ if (settings === undefined)
94
+ return [];
95
+ const fields = [];
96
+ const video = loudnessProblem(settings.videoLufs, "video");
97
+ if (video !== undefined)
98
+ fields.push({ field: "loudness.videoLufs", message: video });
99
+ const files = loudnessProblem(settings.audioFilesLufs, "audioFiles");
100
+ if (files !== undefined)
101
+ fields.push({ field: "loudness.audioFilesLufs", message: files });
102
+ return fields;
103
+ }
104
+ // Whether a run levels and masters its sound: the setting is on and there is a narration.
105
+ export function usesLoudness(config) {
106
+ return config.loudness !== undefined && config.sources.audio !== "off";
107
+ }
108
+ // The settings for a new run from the app-wide default; undefined when it is off.
109
+ export function loudnessOfDefault(value) {
110
+ return value.enabled
111
+ ? { videoLufs: value.videoLufs, audioFilesLufs: value.audioFilesLufs }
112
+ : undefined;
113
+ }
114
+ export function loudnessOfForm(form, fallback) {
115
+ if (!(form?.enabled ?? fallback.enabled))
116
+ return undefined;
117
+ return {
118
+ videoLufs: form?.videoLufs ?? fallback.videoLufs,
119
+ audioFilesLufs: form?.audioFilesLufs ?? fallback.audioFilesLufs,
120
+ };
121
+ }
122
+ // "0 dB (recommended, -14 LUFS)", "-6 dB (50%, -20 LUFS)".
123
+ export function loudnessLabel(lufs, target) {
124
+ const db = dbOfLufs(lufs, target);
125
+ const lufsText = `${formatNumber(lufs)} LUFS`;
126
+ if (db === 0)
127
+ return `0 dB (recommended, ${lufsText})`;
128
+ return `${db > 0 ? "+" : ""}${formatNumber(db)} dB (${String(percentOfDb(db))}%, ${lufsText})`;
129
+ }
130
+ // The loudest minus the quietest; 0 for fewer than two.
131
+ export function spreadOf(values) {
132
+ if (values.length < 2)
133
+ return 0;
134
+ return round(Math.max(...values) - Math.min(...values), 1);
135
+ }
136
+ export function reportText(report) {
137
+ const pieces = `${String(report.pieces)} ${report.pieces === 1 ? "piece" : "pieces"}`;
138
+ const skipped = report.skipped === 0
139
+ ? ""
140
+ : ` (${String(report.skipped)} too short or quiet to measure, left as they were)`;
141
+ return `Levelled ${pieces} to ${formatNumber(report.target)} LUFS${skipped}: the spread was ${formatNumber(report.spreadBefore)} LU, now ${formatNumber(report.spreadAfter)} LU.`;
142
+ }
143
+ // "Mastered to -14 LUFS: measured -14.1 LUFS, peaks -1.6 dBTP".
144
+ export function masterText(report) {
145
+ return `Mastered to ${formatNumber(report.target)} LUFS: measured ${formatNumber(report.integrated)} LUFS, peaks ${formatNumber(report.truePeak)} dBTP`;
146
+ }
147
+ function round(value, places) {
148
+ const scale = 10 ** places;
149
+ return Math.round(value * scale) / scale;
150
+ }
151
+ function formatNumber(value) {
152
+ return String(round(value, 1)).replace("-", "−");
153
+ }
@@ -0,0 +1,10 @@
1
+ // The model-upkeep vocabulary, free of the database so the browser can import it too.
2
+ // The four model choices a run makes, and the catalogue list each one comes from.
3
+ export const usageSlots = ["llm", "audio", "images", "animate"];
4
+ export const usageKinds = ["template", "schedule", "draft", "project"];
5
+ export const slotLabels = {
6
+ llm: "Text model",
7
+ audio: "Voice model",
8
+ images: "Image model",
9
+ animate: "Animation model",
10
+ };
@@ -0,0 +1,217 @@
1
+ import { transact } from "../../kernel/db/tx.js";
2
+ import { projectById, stagesOf, updateProjectConfig } from "../admission/repo.js";
3
+ import { draftRow } from "../play-drafts/repo.js";
4
+ import { documentOf, saveDraft } from "../play-drafts/service.js";
5
+ import { templateById } from "../project-templates/repo.js";
6
+ import { updateTemplate } from "../project-templates/service.js";
7
+ import { ensureBaseline, saveRevision } from "../revisions/index.js";
8
+ import { currentRevisionId } from "../revisions/repo.js";
9
+ import { configChoices, formChoices, retiredModelUsage, retiredStatus, } from "./usage.js";
10
+ function keepThinking(catalogue, provider, model, thinking) {
11
+ if (thinking === undefined)
12
+ return {};
13
+ const row = catalogue.llm.find((entry) => entry.provider === provider && entry.id === model);
14
+ return row?.llm.thinking?.[thinking] === undefined ? {} : { thinking };
15
+ }
16
+ function matches(choice, from) {
17
+ return choice !== undefined && choice.provider === from.provider && choice.model === from.model;
18
+ }
19
+ // The form with one model choice replaced, or undefined when the form no longer picks `from`
20
+ // there (it was changed meanwhile, so there is nothing to switch).
21
+ export function switchForm(form, catalogue, request) {
22
+ if (!matches(formChoices(form)[request.slot], request.from))
23
+ return undefined;
24
+ const { to } = request;
25
+ switch (request.slot) {
26
+ case "llm": {
27
+ const { thinking, ...rest } = form.llm;
28
+ return {
29
+ ...form,
30
+ llm: { ...rest, model: to, ...keepThinking(catalogue, rest.provider, to, thinking) },
31
+ };
32
+ }
33
+ case "audio":
34
+ return { ...form, audio: { ...form.audio, model: to } };
35
+ case "images":
36
+ return { ...form, images: { ...form.images, model: to } };
37
+ case "animate":
38
+ return form.videoEdit === undefined
39
+ ? undefined
40
+ : { ...form, videoEdit: { ...form.videoEdit, animateModel: to } };
41
+ }
42
+ }
43
+ export function switchConfig(config, catalogue, request) {
44
+ if (!matches(configChoices(config)[request.slot], request.from))
45
+ return undefined;
46
+ const { to } = request;
47
+ switch (request.slot) {
48
+ case "llm": {
49
+ if (config.llm === undefined)
50
+ return undefined;
51
+ const { thinking, ...rest } = config.llm;
52
+ return {
53
+ ...config,
54
+ llm: { ...rest, model: to, ...keepThinking(catalogue, rest.provider, to, thinking) },
55
+ };
56
+ }
57
+ case "audio":
58
+ return config.audio === undefined
59
+ ? undefined
60
+ : { ...config, audio: { ...config.audio, model: to } };
61
+ case "images":
62
+ return config.images === undefined
63
+ ? undefined
64
+ : { ...config, images: { ...config.images, model: to } };
65
+ case "animate":
66
+ return config.videoEdit === undefined
67
+ ? undefined
68
+ : { ...config, videoEdit: { ...config.videoEdit, animateModel: to } };
69
+ }
70
+ }
71
+ const changedMeanwhile = "This changed while you were looking at it. Reload Settings → Models to see what still uses a retired model.";
72
+ // One click on "Switch to <model>": changes that one choice and nothing else. A template gets a
73
+ // new version, and the schedules that ran its latest version follow it to the new one.
74
+ export async function switchRetiredModel(deps, catalogue, request) {
75
+ if (retiredStatus(catalogue, request.slot, {
76
+ provider: request.from.provider,
77
+ model: request.to,
78
+ }) !== null)
79
+ return {
80
+ ok: false,
81
+ message: `${request.to} is not an active model of this provider. Reload Settings → Models and use the suggested switch, or choose a model yourself in the ${request.kind}.`,
82
+ };
83
+ switch (request.kind) {
84
+ case "template":
85
+ return switchTemplate(deps, catalogue, request, request.id);
86
+ case "schedule": {
87
+ const row = deps.db
88
+ .prepare("SELECT s.template_id AS templateId,s.template_version AS version,t.head_version AS head FROM schedules s JOIN project_templates t ON t.id=s.template_id WHERE s.id=? AND s.deleted_at IS NULL")
89
+ .get(request.id);
90
+ if (row === undefined)
91
+ return { ok: false, message: changedMeanwhile };
92
+ if (row.version !== row.head)
93
+ return {
94
+ ok: false,
95
+ message: "This schedule runs an older version of its template. In Library → Schedules, choose Edit, pick the template again so it runs the latest version, then save.",
96
+ };
97
+ return switchTemplate(deps, catalogue, request, String(row.templateId));
98
+ }
99
+ case "draft":
100
+ return switchDraft(deps, catalogue, request);
101
+ case "project":
102
+ return switchProject(deps, catalogue, request);
103
+ }
104
+ }
105
+ function switchTemplate(deps, catalogue, request, templateId) {
106
+ return transact(deps.db, () => {
107
+ const template = templateById(deps.db, templateId);
108
+ if (template === undefined)
109
+ return { ok: false, message: changedMeanwhile };
110
+ const form = switchForm(template.document.form, catalogue, request);
111
+ if (form === undefined)
112
+ return { ok: true, changed: false };
113
+ const saved = updateTemplate(deps, {
114
+ id: template.id,
115
+ name: template.name,
116
+ document: { ...template.document, form },
117
+ baseVersion: template.version,
118
+ mutationId: deps.uuid(),
119
+ });
120
+ if (!saved.ok)
121
+ return {
122
+ ok: false,
123
+ message: saved.reason === "missing-prompt"
124
+ ? `The template "${template.name}" uses a prompt or intro/outro that was deleted, so it cannot be saved. Open it in Library → Templates, choose another, and save.`
125
+ : changedMeanwhile,
126
+ };
127
+ deps.db
128
+ .prepare("UPDATE schedules SET template_version=?,version=version+1,updated_at=? WHERE template_id=? AND template_version=? AND deleted_at IS NULL AND status IN ('active','paused')")
129
+ .run(saved.value.version, deps.clock.now().toISOString(), template.id, template.version);
130
+ return { ok: true, changed: true };
131
+ });
132
+ }
133
+ function switchDraft(deps, catalogue, request) {
134
+ const row = draftRow(deps.db, request.id);
135
+ const document = row === undefined ? null : documentOf(row);
136
+ if (row === undefined || document === null || row.state !== "active")
137
+ return { ok: false, message: changedMeanwhile };
138
+ const form = switchForm(document.form, catalogue, request);
139
+ if (form === undefined)
140
+ return { ok: true, changed: false };
141
+ const saved = saveDraft({ ...deps, uuid: deps.uuid }, {
142
+ id: row.id,
143
+ baseVersion: row.version,
144
+ mutationId: deps.uuid(),
145
+ document: { ...document, form },
146
+ });
147
+ return saved.ok ? { ok: true, changed: true } : { ok: false, message: changedMeanwhile };
148
+ }
149
+ async function switchProject(deps, catalogue, request) {
150
+ const project = projectById(deps.db, request.id);
151
+ if (project === undefined)
152
+ return { ok: false, message: changedMeanwhile };
153
+ if (stagesOf(deps.db, project.id).some((stage) => stage.state === "running"))
154
+ return {
155
+ ok: false,
156
+ message: "This project is running. Wait for it to finish or pause it, then switch.",
157
+ };
158
+ if (currentRevisionId(deps.db, project.id) === undefined) {
159
+ // A project never edited has no saved versions yet: its configuration is the run itself.
160
+ const config = switchConfig(project.config, catalogue, request);
161
+ if (config === undefined)
162
+ return { ok: true, changed: false };
163
+ updateProjectConfig(deps.db, project.id, config, deps.clock.now().toISOString());
164
+ return { ok: true, changed: true };
165
+ }
166
+ const baseline = await ensureBaseline(deps, project.id);
167
+ if (!baseline.ok)
168
+ return { ok: false, message: changedMeanwhile };
169
+ const { revision } = baseline.view;
170
+ const config = switchConfig(revision.config, catalogue, request);
171
+ if (config === undefined)
172
+ return { ok: true, changed: false };
173
+ // The same save Edit project makes: a new version with only the model changed.
174
+ const saved = await saveRevision(deps, {
175
+ projectId: project.id,
176
+ baseRevisionId: revision.id,
177
+ idempotencyKey: deps.uuid(),
178
+ edit: { config, content: revision.content },
179
+ });
180
+ return saved.ok
181
+ ? { ok: true, changed: true }
182
+ : {
183
+ ok: false,
184
+ message: `Slopify could not save the new model on "${project.title}"${saved.reason === "conflict"
185
+ ? " because the project changed meanwhile"
186
+ : "fields" in saved && saved.fields?.[0] !== undefined
187
+ ? ` (${saved.fields[0].message})`
188
+ : ""}. Open the project, choose Edit project and change it under Providers.`,
189
+ };
190
+ }
191
+ // "Switch all": every listed use that has a suggestion and no reason to hold back, one by one.
192
+ export async function switchAllRetired(deps, catalogue) {
193
+ let switched = 0;
194
+ const failed = [];
195
+ // Templates first: switching one also moves the schedules that follow it.
196
+ const order = ["template", "schedule", "draft", "project"];
197
+ const usages = [...retiredModelUsage(deps.db, catalogue)].sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind));
198
+ for (const usage of usages) {
199
+ if (usage.blocked !== null || usage.replacement === null)
200
+ continue;
201
+ const result = await switchRetiredModel(deps, catalogue, requestOf(usage));
202
+ if (!result.ok)
203
+ failed.push({ key: usage.key, message: result.message });
204
+ else if (result.changed)
205
+ switched++;
206
+ }
207
+ return { switched, failed };
208
+ }
209
+ export function requestOf(usage) {
210
+ return {
211
+ kind: usage.kind,
212
+ id: usage.id,
213
+ slot: usage.slot,
214
+ from: { provider: usage.provider, model: usage.model },
215
+ to: usage.replacement?.id ?? "",
216
+ };
217
+ }