@gentbajko/slopify 2.4.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 (469) 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 +11 -1
  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 +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -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-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
package/dist/main.js CHANGED
@@ -11,13 +11,15 @@ import { createHostCliClient } from "./adapters/host-cli/index.js";
11
11
  import { nodeRunCli } from "./adapters/llm/run-cli.js";
12
12
  import { curateRegistry } from "./catalog/registry.js";
13
13
  import { createCatalogueStore } from "./catalog/store.js";
14
- import { dockerActivationCommitted, dockerFolderConfiguration, } from "./edge/docker-projects/activation.js";
15
- import { createHub } from "./edge/events/hub.js";
14
+ import { createAutostart } from "./edge/autostart/index.js";
15
+ import { dockerActivationCommitted, dockerFilesLayout, dockerFolderConfiguration, } from "./edge/docker-install/activation.js";
16
+ import { createHub, observedHub } from "./edge/events/hub.js";
16
17
  import { currentProjectEvent } from "./edge/events/visibility.js";
17
18
  import { createApp } from "./edge/http/app.js";
18
19
  import { createMutationLifecycle, drainMutationsWithDeadline } from "./edge/http/mutations.js";
19
20
  import { limitRequestTimes } from "./edge/http/timeouts.js";
20
21
  import { openFolder } from "./edge/open-folder.js";
22
+ import { readHostLogin } from "./host-cli/status.js";
21
23
  import { createAudioPreviewStore } from "./kernel/audio-preview.js";
22
24
  import { systemClock } from "./kernel/clock.js";
23
25
  import { openDb } from "./kernel/db/index.js";
@@ -27,34 +29,57 @@ import { causedBy } from "./kernel/errors.js";
27
29
  import { ulidIds } from "./kernel/ids.js";
28
30
  import { acquireInstanceLock } from "./kernel/lock.js";
29
31
  import { openLog } from "./kernel/log.js";
30
- import { ensureDirs, layout, subtitleModelDir } from "./kernel/paths.js";
32
+ import { ensureDataDirs, ensureDirs, layout, repoint, subtitleModelDir } from "./kernel/paths.js";
31
33
  import { stageKinds } from "./kernel/pipeline.js";
32
34
  import { sqliteAttempts } from "./kernel/runner/attempt-repo.js";
35
+ import { auditionVoice } from "./kernel/runner/audition.js";
33
36
  import { createCheckpointAuthority, } from "./kernel/runner/checkpoint-authority.js";
34
37
  import { createRunner } from "./kernel/runner/index.js";
35
38
  import { stageProviders } from "./kernel/runner/providers.js";
36
39
  import { createProviderQueue } from "./kernel/runner/queue.js";
40
+ import { standaloneImage, standaloneLlm } from "./kernel/runner/standalone.js";
37
41
  import { readVersion } from "./kernel/version.js";
38
42
  import { modelSources } from "./model-catalog.js";
39
- import { projectPaused } from "./slices/admission/repo.js";
43
+ import { projectById, projectPaused, projectTrashed } from "./slices/admission/repo.js";
44
+ import { createBackupService } from "./slices/backups/service.js";
40
45
  import { pumpQueue, queueWaiting } from "./slices/batch/index.js";
46
+ import { settleInterruptedCastImages } from "./slices/channels/cast-images.js";
41
47
  import { approveCheckpoint } from "./slices/checkpoints/index.js";
42
48
  import { checkpointDecisionForWork, recoverCheckpointWork, settleReleasedCheckpoints, } from "./slices/checkpoints/recovery.js";
49
+ import { createEpisodeMemoryWatcher, } from "./slices/episodes/summarize.js";
43
50
  import { resolveFont } from "./slices/fonts/index.js";
51
+ import { decodePeaks } from "./slices/narration/peaks.js";
52
+ import { createRunNotifier } from "./slices/notifications/notifier.js";
53
+ import { createNotificationSender } from "./slices/notifications/send.js";
54
+ import { readNotificationUrl } from "./slices/notifications/settings.js";
55
+ import { seedSamples } from "./slices/onboarding/sample.js";
44
56
  import { templateById } from "./slices/project-templates/repo.js";
57
+ import { recoverProject } from "./slices/rebuild/recovery.js";
45
58
  import { claimWork, finishWork, maySubmit } from "./slices/rebuild/repo.js";
59
+ import { waitToRetry, wakeRetries } from "./slices/rebuild/retry.js";
60
+ import { createReviewRedos, reviewHold } from "./slices/rebuild/review-redo.js";
46
61
  import { materializeAdmittedWork } from "./slices/rebuild/runtime-materialize.js";
47
62
  import { runRevisionInvocation } from "./slices/rebuild/runtime-run.js";
48
63
  import { executionStages, executionStandings, invocationReady, projectStandings, recordWorkProgress, } from "./slices/rebuild/runtime-store.js";
64
+ import { currentRevisionId } from "./slices/revisions/repo.js";
65
+ import { createLimitGate, resumeAfterRestart } from "./slices/run-cost/limits.js";
66
+ import { createStandaloneMeter, createUsageMeter } from "./slices/run-cost/meter.js";
49
67
  import { settleTerminalScheduleRuns } from "./slices/schedules/repo.js";
50
68
  import { createScheduleRunner } from "./slices/schedules/scheduler.js";
51
69
  import { nodeCliProbe } from "./slices/settings/cli-status.js";
52
- import { isLocalCliProvider } from "./slices/settings/model.js";
70
+ import { isLocalCliProvider, localCliConcurrency } from "./slices/settings/model.js";
53
71
  import { providerStatuses } from "./slices/settings/readiness.js";
72
+ import { busyProjects } from "./slices/storage/backup-export.js";
73
+ import { documentsDir, nodeDocumentsHost } from "./slices/storage/documents.js";
74
+ import { createFilesService, ensureFilesFolders, filesLayoutOf, settleFilesLocation, } from "./slices/storage/files-location.js";
54
75
  import { reconcileStorage } from "./slices/storage/reconcile.js";
76
+ import { previewPictures } from "./slices/style-preview/images.js";
77
+ import { ffmpegStylePreview } from "./slices/style-preview/render.js";
78
+ import { createStylePreviews, stylePreviewDir } from "./slices/style-preview/service.js";
55
79
  import { collectorEndpoint, httpPostEvents } from "./slices/telemetry/collector-client.js";
56
80
  import { createFlusher } from "./slices/telemetry/flush.js";
57
81
  import { record } from "./slices/telemetry/record.js";
82
+ import { createTrashPurge } from "./slices/trash/service.js";
58
83
  import { probeDurationMs } from "./slices/video/ffmpeg.js";
59
84
  import { watchActivation } from "./updater/candidate.js";
60
85
  import { launchUpdate } from "./updater/install.js";
@@ -121,11 +146,12 @@ export async function boot(config, options = {}) {
121
146
  if (dockerState !== undefined &&
122
147
  (process.env.SLOPIFY_CONTAINER !== "1" ||
123
148
  dockerState !== "/opt/slopify-install/activation.json"))
124
- throw new Error("This container was started with Slopify's Docker settings in the wrong place (SLOPIFY_DOCKER_INSTALL_STATE). Start it with the launcher instead: npx @gentbajko/slopify --docker");
149
+ throw new Error("This container was started with Slopify's Docker settings in the wrong place (SLOPIFY_DOCKER_INSTALL_STATE must be /opt/slopify-install/activation.json). Start it with Slopify's compose.yaml, or install it with npx @gentbajko/slopify --docker");
125
150
  const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
126
151
  const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
127
- ensureDirs(paths, { mode: 0o700 });
152
+ ensureDataDirs(paths, { mode: 0o700 });
128
153
  const lock = acquireInstanceLock(paths.lock);
154
+ const freshInstall = !existsSync(paths.db);
129
155
  let db;
130
156
  try {
131
157
  const ffmpeg = await prepareFfmpeg({
@@ -136,10 +162,29 @@ export async function boot(config, options = {}) {
136
162
  });
137
163
  db = openDb(paths.db);
138
164
  migrate(db, clock);
165
+ // Where the projects, backups and exports live. A container's are the installer's mounts;
166
+ // a native install decides once and remembers (slices/storage/files-location.ts).
167
+ const container = process.env.SLOPIFY_CONTAINER === "1";
168
+ const files = container
169
+ ? dockerFilesLayout(process.env, paths.dataDir)
170
+ : filesLayoutOf(await settleFilesLocation({
171
+ db,
172
+ dataDir: paths.dataDir,
173
+ fresh: freshInstall,
174
+ documents: options.filesInDocuments === true
175
+ ? () => documentsDir(nodeDocumentsHost())
176
+ : undefined,
177
+ }), paths.dataDir);
178
+ repoint(paths, files);
179
+ if (files.exports === null)
180
+ ensureDirs(paths, { mode: 0o700 });
181
+ else
182
+ await ensureFilesFolders(files);
139
183
  const runtimeDb = db;
140
184
  const interrupted = markInterruptedStages(db, clock);
141
185
  recoverCheckpointWork(db);
142
186
  const settledSchedules = settleTerminalScheduleRuns(db, clock.now().toISOString());
187
+ settleInterruptedCastImages({ db });
143
188
  // The updater can restore the database after a failed candidate boot, but it cannot
144
189
  // restore files deleted by reconciliation. Leave the filesystem untouched until the
145
190
  // candidate's committed activation pointer has been verified.
@@ -151,10 +196,28 @@ export async function boot(config, options = {}) {
151
196
  : `interrupted stages ${interrupted}, settled schedules ${settledSchedules}, orphan files ${reconciled.orphanFiles}, staged files ${reconciled.stagedFiles}`,
152
197
  });
153
198
  const eventDb = db;
154
- const hub = createHub({
199
+ const sendNotification = createNotificationSender(globalThis.fetch);
200
+ const notifier = createRunNotifier({
201
+ url: () => readNotificationUrl(eventDb),
202
+ subject: (projectId) => {
203
+ const project = projectById(eventDb, projectId);
204
+ return project === undefined
205
+ ? undefined
206
+ : { title: project.title, makesVideo: project.config.sources.video !== "off" };
207
+ },
208
+ send: sendNotification,
209
+ log,
210
+ });
211
+ // Episode memory summarises a project that has just finished; wired once the providers
212
+ // exist, below.
213
+ let episodes;
214
+ const hub = observedHub(createHub({
155
215
  ids,
156
216
  log,
157
217
  acceptEvent: (event) => currentProjectEvent(eventDb, event),
218
+ }), (event) => {
219
+ notifier.observe(event);
220
+ episodes?.observe(event);
158
221
  });
159
222
  const version = readVersion();
160
223
  const telemetry = { db, ids, clock, log, appVersion: version };
@@ -176,8 +239,26 @@ export async function boot(config, options = {}) {
176
239
  probe: nodeCliProbe,
177
240
  hostCli,
178
241
  }), catalogue);
242
+ // Calls that belong to no project (topic generation, episode summaries, cast pictures)
243
+ // still go through the attempt wrapper, and are metered against their schedule or channel.
244
+ const standalone = {
245
+ registry,
246
+ clock,
247
+ log,
248
+ meter: createStandaloneMeter({ db, ids, clock, catalogue: () => catalogue.read() }),
249
+ };
250
+ episodes = createEpisodeMemoryWatcher({
251
+ db,
252
+ paths,
253
+ clock,
254
+ log,
255
+ uuid: randomUUID,
256
+ llm: (call) => standaloneLlm(standalone, call),
257
+ });
179
258
  const audioPreviews = createAudioPreviewStore();
259
+ const reviewRedos = createReviewRedos();
180
260
  const runner = wireRunner({
261
+ onFinished: (work) => reviewRedos.kick(work.projectId),
181
262
  db,
182
263
  paths,
183
264
  clock,
@@ -241,7 +322,7 @@ export async function boot(config, options = {}) {
241
322
  ? Promise.resolve(version)
242
323
  : publishedVersion(globalThis.fetch),
243
324
  unsupported: () => process.env.SLOPIFY_DISABLE_UPDATES === "1"
244
- ? "This container is updated by pulling a new image and recreating it."
325
+ ? "Slopify in Docker is updated from the terminal: npx @gentbajko/slopify@latest update. It waits for running work, keeps a recovery copy of your data and puts the previous version back if the new one doesn't start."
245
326
  : !existsSync(oldEntry) || !existsSync(workerEntry)
246
327
  ? "Run Slopify from its installed package to use in-app updates."
247
328
  : npm === undefined
@@ -253,6 +334,18 @@ export async function boot(config, options = {}) {
253
334
  .prepare("SELECT id FROM projects")
254
335
  .all()
255
336
  .some((row) => typeof row.id === "string" && runner.hasInflight?.(row.id) === true),
337
+ busyWith: () => {
338
+ const running = updateDb
339
+ .prepare("SELECT p.title FROM stages s JOIN projects p ON p.id=s.project_id WHERE s.state='running' LIMIT 1")
340
+ .get();
341
+ if (typeof running?.title === "string")
342
+ return running.title;
343
+ const inflight = updateDb
344
+ .prepare("SELECT id,title FROM projects")
345
+ .all()
346
+ .find((row) => typeof row.id === "string" && runner.hasInflight?.(row.id) === true);
347
+ return typeof inflight?.title === "string" ? inflight.title : undefined;
348
+ },
256
349
  report: (message) => log.write("warn", "update", { detail: message }),
257
350
  install: async (next, restarting) => {
258
351
  if (npm === undefined)
@@ -286,6 +379,21 @@ export async function boot(config, options = {}) {
286
379
  modelsFor: modelSources(registry).modelsFor,
287
380
  emit: (projectId, event) => hub.emit(projectId, event),
288
381
  };
382
+ // Redos a review asked for before the last shutdown start now.
383
+ reviewRedos.bind(rebuild);
384
+ reviewRedos.kick();
385
+ // A stage that was waiting for a CLI's plan limits when the app stopped carries on waiting.
386
+ void resumeAfterRestart(db, async (projectId) => {
387
+ const baseRevisionId = currentRevisionId(runtimeDb, projectId);
388
+ if (baseRevisionId === undefined)
389
+ return false;
390
+ const result = await recoverProject(rebuild, projectId, {
391
+ baseRevisionId,
392
+ idempotencyKey: randomUUID(),
393
+ action: { kind: "resume" },
394
+ });
395
+ return result.ok;
396
+ }, log);
289
397
  const draftDeps = {
290
398
  db,
291
399
  paths,
@@ -305,11 +413,20 @@ export async function boot(config, options = {}) {
305
413
  flusher.soon();
306
414
  },
307
415
  };
308
- const scheduleDeps = {
416
+ const scheduleRunnerDeps = {
309
417
  ...draftDeps,
310
418
  template: (id, templateVersion) => templateById(runtimeDb, id, templateVersion),
419
+ topicLlm: (call) => standaloneLlm(standalone, call),
420
+ topicsWaiting: (event) => {
421
+ hub.emitGlobal(event);
422
+ notifier.observeTopics(event);
423
+ },
424
+ };
425
+ const scheduleRunner = createScheduleRunner(scheduleRunnerDeps);
426
+ const scheduleDeps = {
427
+ ...scheduleRunnerDeps,
428
+ requestTopics: scheduleRunner.requestTopics,
311
429
  };
312
- const scheduleRunner = createScheduleRunner(scheduleDeps);
313
430
  scheduleRunner.recover(clock.now());
314
431
  const scheduleTicks = createScheduleTickLifecycle({
315
432
  beginMutation: updater.beginMutation,
@@ -319,14 +436,67 @@ export async function boot(config, options = {}) {
319
436
  }),
320
437
  });
321
438
  const mutations = createMutationLifecycle();
439
+ const folderConfiguration = await dockerFolderConfiguration(process.env, paths.projects);
440
+ const filesService = createFilesService({
441
+ db,
442
+ paths,
443
+ documents: () => documentsDir(nodeDocumentsHost()),
444
+ busy: () => busyProjects({ db: runtimeDb, hasInflight: runner.hasInflight }),
445
+ openFolder,
446
+ beginMutation: updater.beginMutation,
447
+ ...(container
448
+ ? {
449
+ docker: {
450
+ hostProjects: folderConfiguration.hostProjects,
451
+ hostBackups: folderConfiguration.hostBackups,
452
+ ...(hostCli === undefined ? {} : { openOnHost: hostCli.openFolder }),
453
+ },
454
+ }
455
+ : {}),
456
+ });
457
+ const backups = createBackupService({
458
+ db,
459
+ paths,
460
+ clock,
461
+ ids,
462
+ log,
463
+ appVersion: version,
464
+ hasInflight: runner.hasInflight,
465
+ location: folderConfiguration,
466
+ bootedAt: clock.now(),
467
+ beginMutation: updater.beginMutation,
468
+ });
469
+ // Before the server answers, so the first page already lists the samples. A failure costs
470
+ // only the samples: the log says why and Settings → Restore samples tries again.
471
+ if (options.seedSample === true && !pendingActivation)
472
+ await seedSamples({ db, paths, clock, ids, log, appVersion: version }).catch((error) => {
473
+ log.write("warn", "sample.seed", { detail: causedBy(error) });
474
+ });
475
+ const autostart = await createAutostart({
476
+ db,
477
+ dataDir: paths.dataDir,
478
+ logs: paths.logs,
479
+ port: config.port,
480
+ host: config.host,
481
+ version,
482
+ env: process.env,
483
+ });
484
+ if (options.refreshAutostart === true)
485
+ void autostart.refresh().catch((error) => {
486
+ log.write("warn", "autostart.refresh", { detail: causedBy(error) });
487
+ });
322
488
  const app = createApp({
489
+ autostart,
323
490
  rebuild,
324
491
  drafts: draftDeps,
325
492
  schedules: scheduleDeps,
493
+ backups,
494
+ files: filesService,
326
495
  ...(rebuild.measureAudio === undefined ? {} : { measureAudio: rebuild.measureAudio }),
496
+ decodePeaks: (path, signal) => decodePeaks(ffmpeg, path, signal),
327
497
  openFolder,
328
498
  folderLocation: {
329
- ...(await dockerFolderConfiguration(process.env, paths.projects)),
499
+ ...folderConfiguration,
330
500
  ...(hostCli === undefined ? {} : { openOnHost: hostCli.openFolder }),
331
501
  },
332
502
  ...(dockerState === undefined ? {} : { installationPending: () => updater.locked() }),
@@ -336,8 +506,24 @@ export async function boot(config, options = {}) {
336
506
  runner,
337
507
  updater,
338
508
  mutations,
509
+ sendNotification,
339
510
  audioPreviews,
511
+ stylePreviews: createStylePreviews({
512
+ dir: stylePreviewDir(paths.dataDir),
513
+ render: ffmpegStylePreview({ ffmpeg, paths, log, dir: stylePreviewDir(paths.dataDir) }),
514
+ pictures: previewPictures({ db, paths }),
515
+ log,
516
+ }),
340
517
  ...modelSources(registry),
518
+ audition: (call, signal) => auditionVoice({ registry, clock, log }, call, signal),
519
+ voiceLanguages: async (provider, voiceId, signal) => {
520
+ try {
521
+ return await registry.tts(provider).voiceLanguages?.(voiceId, signal);
522
+ }
523
+ catch {
524
+ return undefined;
525
+ }
526
+ },
341
527
  catalogue,
342
528
  clock,
343
529
  ids,
@@ -347,6 +533,13 @@ export async function boot(config, options = {}) {
347
533
  flushSoon: flusher.soon,
348
534
  probe: nodeCliProbe,
349
535
  hostCliStatus: hostCli?.status,
536
+ generateCastImage: ({ channelId, ...call }) => standaloneImage(standalone, {
537
+ ...call,
538
+ owner: { kind: "channel", id: channelId },
539
+ purpose: "cast-image",
540
+ }),
541
+ fetch: globalThis.fetch,
542
+ ...(hostCli === undefined ? { cliLogin: readHostLogin } : {}),
350
543
  });
351
544
  const server = await listen(app, config, log);
352
545
  const queueTimer = setInterval(() => {
@@ -366,7 +559,70 @@ export async function boot(config, options = {}) {
366
559
  const scheduleTimer = setInterval(() => {
367
560
  void scheduleTicks.tick();
368
561
  }, 15_000);
562
+ // A step waiting out a rate limit or a timeout runs again once its wait is over; the wait
563
+ // is in the database, so this also picks up the ones a restart left waiting.
564
+ const retryTimer = setInterval(() => {
565
+ const release = updater.beginMutation();
566
+ if (!release)
567
+ return;
568
+ try {
569
+ wakeRetries(updateDb, clock, runner);
570
+ }
571
+ catch (error) {
572
+ log.write("error", "stage.retry", { detail: causedBy(error) });
573
+ }
574
+ finally {
575
+ release();
576
+ }
577
+ }, 5_000);
369
578
  void scheduleTicks.tick();
579
+ // Once a minute is plenty for a daily slot; the decision itself (slices/backups/schedule)
580
+ // holds the first run back for a couple of minutes after a start.
581
+ // The model catalogue keeps itself current: once at start, then whenever a day has passed
582
+ // since the last check (looked at hourly, so a laptop that slept catches up).
583
+ const syncModels = () => {
584
+ if (!options.refreshModels || !catalogue.sync)
585
+ return;
586
+ catalogue.sync().then((status) => {
587
+ if (status.warning !== null)
588
+ log.write("warn", "model-catalog.sync", { detail: status.warning });
589
+ }, (error) => log.write("warn", "model-catalog.sync", { detail: causedBy(error) }));
590
+ };
591
+ syncModels();
592
+ const modelTimer = setInterval(() => {
593
+ if (catalogue.syncDue?.(Date.now()) === true)
594
+ syncModels();
595
+ }, 60 * 60_000);
596
+ const backupTimer = setInterval(() => {
597
+ backups.tick().catch((error) => {
598
+ log.write("error", "backups.tick", { detail: causedBy(error) });
599
+ });
600
+ }, 60_000);
601
+ // Settings → Trash keeps deleted items 30 days; once a day (looked at hourly, and once at
602
+ // start) whatever is older goes for good, a project's folder with it.
603
+ const trashPurge = createTrashPurge({
604
+ db: updateDb,
605
+ paths,
606
+ clock,
607
+ log,
608
+ hasInflight: runner.hasInflight,
609
+ });
610
+ const purgeTrash = () => {
611
+ const release = updater.beginMutation();
612
+ if (!release)
613
+ return;
614
+ try {
615
+ trashPurge.tick();
616
+ }
617
+ catch (error) {
618
+ log.write("error", "trash.purge", { detail: causedBy(error) });
619
+ }
620
+ finally {
621
+ release();
622
+ }
623
+ };
624
+ purgeTrash();
625
+ const trashTimer = setInterval(purgeTrash, 60 * 60_000);
370
626
  listeningPort = portOf(server) ?? config.port;
371
627
  // Whatever last run left queued goes out at start. Nothing waits for
372
628
  // it, and an unreachable collector costs one refused socket.
@@ -388,10 +644,19 @@ export async function boot(config, options = {}) {
388
644
  let stopActivation = () => { };
389
645
  shutdown = () => {
390
646
  stopping ??= (async () => {
647
+ notifier.close();
648
+ episodes?.close();
391
649
  clearInterval(queueTimer);
392
650
  clearInterval(scheduleTimer);
651
+ clearInterval(retryTimer);
652
+ clearInterval(backupTimer);
653
+ clearInterval(trashTimer);
654
+ clearInterval(modelTimer);
393
655
  const mutationDrain = mutations.stop();
394
656
  const scheduleDrain = scheduleTicks.stop();
657
+ const topicDrain = scheduleRunner.stop();
658
+ // A backup being written is stopped and its partial file removed, not waited for.
659
+ const backupDrain = backups.stop();
395
660
  const serverClose = beginServerClose(server);
396
661
  stopActivation();
397
662
  audioPreviews.close();
@@ -401,7 +666,10 @@ export async function boot(config, options = {}) {
401
666
  // database until their response settles; anything racing shutdown receives 503.
402
667
  await drainMutationsWithDeadline(mutationDrain, serverClose.terminate, mutationDrainTimeoutMs);
403
668
  await scheduleDrain;
669
+ await topicDrain;
670
+ await backupDrain;
404
671
  await runner.abortAll();
672
+ await episodes?.settled();
405
673
  }
406
674
  finally {
407
675
  serverClose.terminate();
@@ -425,7 +693,7 @@ export async function boot(config, options = {}) {
425
693
  };
426
694
  if (pendingActivation)
427
695
  stopActivation = watchActivation(updater, candidateToken, shutdown, (message) => log.write("warn", "update", { detail: message }));
428
- return { paths, url: urlOf(config.host, listeningPort), stop: shutdown };
696
+ return { paths, url: urlOf(config.host, listeningPort), stop: shutdown, autostart };
429
697
  }
430
698
  catch (error) {
431
699
  db?.close();
@@ -433,7 +701,7 @@ export async function boot(config, options = {}) {
433
701
  throw error;
434
702
  }
435
703
  }
436
- export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, telemetry, flusher, registry, catalogue, ffmpeg, }) {
704
+ export function wireRunner({ onFinished, audioPreviews, db, paths, clock, ids, log, hub, telemetry, flusher, registry, catalogue, ffmpeg, alignSubtitles: aligner = alignSubtitles, }) {
437
705
  // A stage counts what it did and the queue is flushed after each new event. `record`
438
706
  // swallows its own failures, so this can neither fail a stage nor widen what leaves the
439
707
  // machine - the payload allow-list is checked inside it.
@@ -441,7 +709,17 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
441
709
  record(telemetry, type, counters);
442
710
  flusher.soon();
443
711
  };
444
- const execution = { db, paths, ids, clock, log, ffmpeg, alignSubtitles, audioPreviews, count };
712
+ const execution = {
713
+ db,
714
+ paths,
715
+ ids,
716
+ clock,
717
+ log,
718
+ ffmpeg,
719
+ alignSubtitles: aligner,
720
+ audioPreviews,
721
+ count,
722
+ };
445
723
  // A stage slice is handed the wrapped calls, never the registry: every provider call
446
724
  // it makes is already inside the retry policy (kernel/runner/providers.ts).
447
725
  const providers = {
@@ -449,10 +727,23 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
449
727
  attempts: sqliteAttempts(db, ids),
450
728
  clock,
451
729
  log,
452
- queue: createProviderQueue((provider) => isLocalCliProvider(provider) ? 3 : (catalogue.read().providers[provider]?.maxConcurrent ?? 1)),
730
+ meter: createUsageMeter({ db, ids, clock, catalogue: () => catalogue.read() }),
731
+ limits: createLimitGate({
732
+ db,
733
+ clock,
734
+ log,
735
+ changed: (projectId) => hub.emit(projectId, { type: "project.updated", projectId }),
736
+ }),
737
+ queue: createProviderQueue((provider) => isLocalCliProvider(provider)
738
+ ? localCliConcurrency(provider)
739
+ : (catalogue.read().providers[provider]?.maxConcurrent ?? 1)),
453
740
  };
454
741
  const checkpoints = createCheckpointAuthority({
455
- decide: (work) => checkpointDecisionForWork(execution, work),
742
+ // A review waiting to send its item back holds the item's dependents like a checkpoint.
743
+ decide: (work) => {
744
+ const review = reviewHold(execution, work);
745
+ return review.kind === "held" ? review : checkpointDecisionForWork(execution, work);
746
+ },
456
747
  approve: (projectId, checkpointId, identity) => approveCheckpoint(db, {
457
748
  ...identity,
458
749
  projectId,
@@ -473,20 +764,30 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
473
764
  },
474
765
  standingsOf: (projectId) => executionStandings(execution, projectId),
475
766
  ready: (work) => invocationReady(execution, work),
476
- paused: (projectId) => projectPaused(db, projectId) || queueWaiting(db, projectId),
767
+ // A project in the trash (Settings → Trash) holds like a paused one until it is restored.
768
+ paused: (projectId) => projectPaused(db, projectId) ||
769
+ queueWaiting(db, projectId) ||
770
+ projectTrashed(db, projectId),
477
771
  claim: (work) => {
478
772
  const claimed = claimWork(db, work);
479
773
  projectStandings(execution, work.projectId);
480
774
  return claimed;
481
775
  },
482
776
  maySubmit: (work, pieceId) => maySubmit(db, work, pieceId),
483
- finish: (work, state, reason) => {
777
+ waitToRetry: (work, fault, reason) => transact(db, () => {
778
+ const at = waitToRetry(execution, work, fault, reason, Math.random);
779
+ if (at !== undefined)
780
+ projectStandings(execution, work.projectId);
781
+ return at;
782
+ }),
783
+ finish: (work, state, reason, kind) => {
484
784
  transact(db, () => {
485
- finishWork(db, work, state, reason);
785
+ finishWork(db, work, state, reason, kind ?? null);
486
786
  materializeAdmittedWork(execution, work.projectId);
487
787
  projectStandings(execution, work.projectId);
488
788
  settleReleasedCheckpoints(execution, work.projectId);
489
789
  });
790
+ onFinished?.(work);
490
791
  },
491
792
  },
492
793
  runs: Object.fromEntries(stageKinds.map((kind) => [
@@ -0,0 +1,93 @@
1
+ # Slopify 0.4.2 to 1.0
2
+
3
+ Versions 0.4.3 to 1.0.0, released 9 to 13 September 2026 (0.4.2 shipped on 3 September).
4
+
5
+ This is the stretch where Slopify went from a pipeline that ran once, start to finish, to something you can live with: pause it, change your mind halfway, edit a finished project without paying for everything twice, and leave it running on a schedule. It also picked up free local captions, the Gemini CLI, Inworld voices and an updater. 1.0.0 is the first release where a finished project is never a dead end.
6
+
7
+ ## Highlights
8
+
9
+ - Captions, made on your machine for free: English word timing with a 95 MB speech model, no caption service and no key.
10
+ - Edit any finished project. Slopify keeps every version and remakes only what your change affects.
11
+ - Pause and resume a run, and switch its providers while it is paused.
12
+ - A cost review before anything starts, with the exact inputs, prices and a readiness check.
13
+ - Review checkpoints hold Audio, Images or Video until you approve them, while the rest keeps going.
14
+ - Templates for setups you reuse, and daily, weekly or one-off schedules that run them.
15
+ - Gemini CLI joins Claude Code and Codex as a text provider, using the login you already have.
16
+ - Inworld TTS-2 and TTS-2 Flash voices.
17
+ - Update from inside the app with one click.
18
+
19
+ ## What's new
20
+
21
+ ### Getting started
22
+ - An interactive tutorial walks from API keys to a downloaded video in 18 steps (19 from 0.5.1), highlighting the real controls on each screen. Open it from Tutorial in the navigation or Start tutorial on an empty Projects page.
23
+ - The tutorial remembers where you were across reloads and never starts a run for you.
24
+ - Two ways to install on slopify.stream: `npx @gentbajko/slopify@latest` with nothing to install, or `npm install -g @gentbajko/slopify` and then `slopify`.
25
+
26
+ ### Runs
27
+ - Pause and Resume. Pausing stops active work, keeps every finished piece and survives an app restart.
28
+ - Article is the only required stage. Research, Audio, Images, Thumbnail and Video can each be Off.
29
+ - With Images off and Audio on, the run exports a 48 kHz stereo WAV. With Audio off, it renders a silent MP4 at 5 seconds per image.
30
+ - Images from a saved prompt and prewritten thumbnails start straight away, beside research and writing.
31
+ - Uploaded narration is used as it is.
32
+ - Play is four sections, with a preview of the output shape and caption style next to the controls.
33
+ - Play drafts save to this computer as you type, uploads included, and come back after a restart.
34
+ - Review shows the exact inputs, prices and catalogue date before you start. Nothing runs until Start.
35
+ - Review also checks that every provider, model and voice the run needs is ready.
36
+ - Batches: several title or keyword variants from one setup, run one after another.
37
+ - Provider-aware queues cap how many requests go to each provider at once.
38
+
39
+ ### Editing finished projects
40
+ - Change the settings, article, narration, images or captions of any project, finished or not.
41
+ - Every save is a new version in History. Restore any earlier one.
42
+ - Before a rebuild, Slopify lists the outputs your change affects and what they cost. Work that is unaffected is reused, not paid for again.
43
+ - Each project keeps its own copy of the prompts it was made with, so editing a Library prompt never changes an old project.
44
+ - Edit a single image, a caption line, or the article text by hand.
45
+
46
+ ### Review checkpoints
47
+ - Hold Audio, Images or Video/export for approval. Independent work carries on while one part waits.
48
+ - Approve held work from the project's progress view. Approvals survive restarts and later edits.
49
+
50
+ ### Templates and schedules
51
+ - Save a project's setup as a named template and apply it in Play. Templates keep versions, so an old draft is never changed under you.
52
+ - Schedules run a template once, daily or weekly, in your time zone, with up to 49 keyword variants and an optional spend limit.
53
+ - Each schedule has a run history and Pause, Resume, Cancel and Delete. Missed runs follow a policy you choose, and a run never overlaps the one before it.
54
+
55
+ ### Captions and fonts
56
+ - Free English captions for new and existing narrated projects, timed word by word to the actual speech.
57
+ - SRT, VTT and ASS files, and captions burned into the video.
58
+ - Five caption positions, a font size and a live preview in the real output shape.
59
+ - Bundled Barlow, your installed fonts, or upload a TTF or OTF up to 32 MiB.
60
+ - Changing only the caption style reuses the narration and the WAV instead of making them again.
61
+ - When the narration skips a short passage, captions resync on the following words and add a note beside the caption download saying what was missed.
62
+
63
+ ### Narration
64
+ - Inworld TTS-2 and TTS-2 Flash. TTS-2 takes up to 100,000 characters per job.
65
+ - Split narration by characters as well as by paragraph or word count: 3,000 characters by default, always on a sentence end.
66
+ - Hear narration while it is still being made, and watch the article write itself.
67
+
68
+ ### Providers and models
69
+ - Gemini CLI as a text and research provider, signed in with the login you already have.
70
+ - Set the path to each CLI in Settings when Slopify can't find it on your PATH.
71
+ - Model lists load from the provider or the installed CLI, with a refresh button and space to type a model ID.
72
+ - A curated model catalogue with prices, context sizes and thinking settings, refreshed from GitHub.
73
+
74
+ ### Project page
75
+ - A focused workspace: stage list on the left, the output of the chosen stage beside it, and a direct download of the final file.
76
+ - Overall progress next to each stage's own progress.
77
+ - Open folder next to every download.
78
+
79
+ ### Updates and upkeep
80
+ - An update button checks every 15 minutes and installs on one click. It waits for active work, checks the new version starts, and rolls the database back if it doesn't.
81
+ - Settings exports and imports a backup of your templates, prompts, voices and settings. Provider keys are never included.
82
+ - Disk usage per project, cleanup of files no project uses, and a diagnostics download with no secrets in it.
83
+
84
+ ## Fixes
85
+
86
+ - A missing ffmpeg is downloaded into the app's data folder and checked before any provider work starts.
87
+ - Claude Code no longer applies your personal coding setup to article writing.
88
+ - Google's image errors are read correctly; an image quota of zero fails once with a clear message instead of retrying.
89
+ - Slideshow zoom is faster and travels from 100% to 122.5%.
90
+ - The update indicator no longer sticks on Updating after a check finds nothing new.
91
+ - A stalled or interrupted update recovers its status on the next start.
92
+ - Many fixes to editing and rebuilds: regenerated articles open current, uploaded audio can be replaced by generated narration, and interrupted rebuilds recover their unfinished outputs.
93
+ - Many fixes to Play drafts: unfinished uploads, image counts, empty chunk sizes and discarded drafts are all kept or recovered correctly.