@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
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";
14
+ import { createAutostart } from "./edge/autostart/index.js";
15
+ import { dockerActivationCommitted, dockerFilesLayout, dockerFolderConfiguration, } from "./edge/docker-install/activation.js";
15
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,38 +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 { projectById, projectPaused } from "./slices/admission/repo.js";
43
+ import { projectById, projectPaused, projectTrashed } from "./slices/admission/repo.js";
40
44
  import { createBackupService } from "./slices/backups/service.js";
41
45
  import { pumpQueue, queueWaiting } from "./slices/batch/index.js";
46
+ import { settleInterruptedCastImages } from "./slices/channels/cast-images.js";
42
47
  import { approveCheckpoint } from "./slices/checkpoints/index.js";
43
48
  import { checkpointDecisionForWork, recoverCheckpointWork, settleReleasedCheckpoints, } from "./slices/checkpoints/recovery.js";
49
+ import { createEpisodeMemoryWatcher, } from "./slices/episodes/summarize.js";
44
50
  import { resolveFont } from "./slices/fonts/index.js";
51
+ import { decodePeaks } from "./slices/narration/peaks.js";
45
52
  import { createRunNotifier } from "./slices/notifications/notifier.js";
46
53
  import { createNotificationSender } from "./slices/notifications/send.js";
47
54
  import { readNotificationUrl } from "./slices/notifications/settings.js";
55
+ import { seedSamples } from "./slices/onboarding/sample.js";
48
56
  import { templateById } from "./slices/project-templates/repo.js";
57
+ import { recoverProject } from "./slices/rebuild/recovery.js";
49
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";
50
61
  import { materializeAdmittedWork } from "./slices/rebuild/runtime-materialize.js";
51
62
  import { runRevisionInvocation } from "./slices/rebuild/runtime-run.js";
52
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";
53
67
  import { settleTerminalScheduleRuns } from "./slices/schedules/repo.js";
54
68
  import { createScheduleRunner } from "./slices/schedules/scheduler.js";
55
69
  import { nodeCliProbe } from "./slices/settings/cli-status.js";
56
70
  import { isLocalCliProvider, localCliConcurrency } from "./slices/settings/model.js";
57
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";
58
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";
59
79
  import { collectorEndpoint, httpPostEvents } from "./slices/telemetry/collector-client.js";
60
80
  import { createFlusher } from "./slices/telemetry/flush.js";
61
81
  import { record } from "./slices/telemetry/record.js";
82
+ import { createTrashPurge } from "./slices/trash/service.js";
62
83
  import { probeDurationMs } from "./slices/video/ffmpeg.js";
63
84
  import { watchActivation } from "./updater/candidate.js";
64
85
  import { launchUpdate } from "./updater/install.js";
@@ -125,11 +146,12 @@ export async function boot(config, options = {}) {
125
146
  if (dockerState !== undefined &&
126
147
  (process.env.SLOPIFY_CONTAINER !== "1" ||
127
148
  dockerState !== "/opt/slopify-install/activation.json"))
128
- 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");
129
150
  const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
130
151
  const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
131
- ensureDirs(paths, { mode: 0o700 });
152
+ ensureDataDirs(paths, { mode: 0o700 });
132
153
  const lock = acquireInstanceLock(paths.lock);
154
+ const freshInstall = !existsSync(paths.db);
133
155
  let db;
134
156
  try {
135
157
  const ffmpeg = await prepareFfmpeg({
@@ -140,10 +162,29 @@ export async function boot(config, options = {}) {
140
162
  });
141
163
  db = openDb(paths.db);
142
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);
143
183
  const runtimeDb = db;
144
184
  const interrupted = markInterruptedStages(db, clock);
145
185
  recoverCheckpointWork(db);
146
186
  const settledSchedules = settleTerminalScheduleRuns(db, clock.now().toISOString());
187
+ settleInterruptedCastImages({ db });
147
188
  // The updater can restore the database after a failed candidate boot, but it cannot
148
189
  // restore files deleted by reconciliation. Leave the filesystem untouched until the
149
190
  // candidate's committed activation pointer has been verified.
@@ -167,11 +208,17 @@ export async function boot(config, options = {}) {
167
208
  send: sendNotification,
168
209
  log,
169
210
  });
211
+ // Episode memory summarises a project that has just finished; wired once the providers
212
+ // exist, below.
213
+ let episodes;
170
214
  const hub = observedHub(createHub({
171
215
  ids,
172
216
  log,
173
217
  acceptEvent: (event) => currentProjectEvent(eventDb, event),
174
- }), notifier.observe);
218
+ }), (event) => {
219
+ notifier.observe(event);
220
+ episodes?.observe(event);
221
+ });
175
222
  const version = readVersion();
176
223
  const telemetry = { db, ids, clock, log, appVersion: version };
177
224
  const flusher = createFlusher({
@@ -192,8 +239,26 @@ export async function boot(config, options = {}) {
192
239
  probe: nodeCliProbe,
193
240
  hostCli,
194
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
+ });
195
258
  const audioPreviews = createAudioPreviewStore();
259
+ const reviewRedos = createReviewRedos();
196
260
  const runner = wireRunner({
261
+ onFinished: (work) => reviewRedos.kick(work.projectId),
197
262
  db,
198
263
  paths,
199
264
  clock,
@@ -257,7 +322,7 @@ export async function boot(config, options = {}) {
257
322
  ? Promise.resolve(version)
258
323
  : publishedVersion(globalThis.fetch),
259
324
  unsupported: () => process.env.SLOPIFY_DISABLE_UPDATES === "1"
260
- ? "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."
261
326
  : !existsSync(oldEntry) || !existsSync(workerEntry)
262
327
  ? "Run Slopify from its installed package to use in-app updates."
263
328
  : npm === undefined
@@ -269,6 +334,18 @@ export async function boot(config, options = {}) {
269
334
  .prepare("SELECT id FROM projects")
270
335
  .all()
271
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
+ },
272
349
  report: (message) => log.write("warn", "update", { detail: message }),
273
350
  install: async (next, restarting) => {
274
351
  if (npm === undefined)
@@ -302,6 +379,21 @@ export async function boot(config, options = {}) {
302
379
  modelsFor: modelSources(registry).modelsFor,
303
380
  emit: (projectId, event) => hub.emit(projectId, event),
304
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);
305
397
  const draftDeps = {
306
398
  db,
307
399
  paths,
@@ -321,11 +413,20 @@ export async function boot(config, options = {}) {
321
413
  flusher.soon();
322
414
  },
323
415
  };
324
- const scheduleDeps = {
416
+ const scheduleRunnerDeps = {
325
417
  ...draftDeps,
326
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,
327
429
  };
328
- const scheduleRunner = createScheduleRunner(scheduleDeps);
329
430
  scheduleRunner.recover(clock.now());
330
431
  const scheduleTicks = createScheduleTickLifecycle({
331
432
  beginMutation: updater.beginMutation,
@@ -336,6 +437,23 @@ export async function boot(config, options = {}) {
336
437
  });
337
438
  const mutations = createMutationLifecycle();
338
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
+ });
339
457
  const backups = createBackupService({
340
458
  db,
341
459
  paths,
@@ -348,12 +466,34 @@ export async function boot(config, options = {}) {
348
466
  bootedAt: clock.now(),
349
467
  beginMutation: updater.beginMutation,
350
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
+ });
351
488
  const app = createApp({
489
+ autostart,
352
490
  rebuild,
353
491
  drafts: draftDeps,
354
492
  schedules: scheduleDeps,
355
493
  backups,
494
+ files: filesService,
356
495
  ...(rebuild.measureAudio === undefined ? {} : { measureAudio: rebuild.measureAudio }),
496
+ decodePeaks: (path, signal) => decodePeaks(ffmpeg, path, signal),
357
497
  openFolder,
358
498
  folderLocation: {
359
499
  ...folderConfiguration,
@@ -368,7 +508,22 @@ export async function boot(config, options = {}) {
368
508
  mutations,
369
509
  sendNotification,
370
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
+ }),
371
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
+ },
372
527
  catalogue,
373
528
  clock,
374
529
  ids,
@@ -378,6 +533,13 @@ export async function boot(config, options = {}) {
378
533
  flushSoon: flusher.soon,
379
534
  probe: nodeCliProbe,
380
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 } : {}),
381
543
  });
382
544
  const server = await listen(app, config, log);
383
545
  const queueTimer = setInterval(() => {
@@ -397,14 +559,70 @@ export async function boot(config, options = {}) {
397
559
  const scheduleTimer = setInterval(() => {
398
560
  void scheduleTicks.tick();
399
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);
400
578
  void scheduleTicks.tick();
401
579
  // Once a minute is plenty for a daily slot; the decision itself (slices/backups/schedule)
402
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);
403
596
  const backupTimer = setInterval(() => {
404
597
  backups.tick().catch((error) => {
405
598
  log.write("error", "backups.tick", { detail: causedBy(error) });
406
599
  });
407
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);
408
626
  listeningPort = portOf(server) ?? config.port;
409
627
  // Whatever last run left queued goes out at start. Nothing waits for
410
628
  // it, and an unreachable collector costs one refused socket.
@@ -427,11 +645,16 @@ export async function boot(config, options = {}) {
427
645
  shutdown = () => {
428
646
  stopping ??= (async () => {
429
647
  notifier.close();
648
+ episodes?.close();
430
649
  clearInterval(queueTimer);
431
650
  clearInterval(scheduleTimer);
651
+ clearInterval(retryTimer);
432
652
  clearInterval(backupTimer);
653
+ clearInterval(trashTimer);
654
+ clearInterval(modelTimer);
433
655
  const mutationDrain = mutations.stop();
434
656
  const scheduleDrain = scheduleTicks.stop();
657
+ const topicDrain = scheduleRunner.stop();
435
658
  // A backup being written is stopped and its partial file removed, not waited for.
436
659
  const backupDrain = backups.stop();
437
660
  const serverClose = beginServerClose(server);
@@ -443,8 +666,10 @@ export async function boot(config, options = {}) {
443
666
  // database until their response settles; anything racing shutdown receives 503.
444
667
  await drainMutationsWithDeadline(mutationDrain, serverClose.terminate, mutationDrainTimeoutMs);
445
668
  await scheduleDrain;
669
+ await topicDrain;
446
670
  await backupDrain;
447
671
  await runner.abortAll();
672
+ await episodes?.settled();
448
673
  }
449
674
  finally {
450
675
  serverClose.terminate();
@@ -468,7 +693,7 @@ export async function boot(config, options = {}) {
468
693
  };
469
694
  if (pendingActivation)
470
695
  stopActivation = watchActivation(updater, candidateToken, shutdown, (message) => log.write("warn", "update", { detail: message }));
471
- return { paths, url: urlOf(config.host, listeningPort), stop: shutdown };
696
+ return { paths, url: urlOf(config.host, listeningPort), stop: shutdown, autostart };
472
697
  }
473
698
  catch (error) {
474
699
  db?.close();
@@ -476,7 +701,7 @@ export async function boot(config, options = {}) {
476
701
  throw error;
477
702
  }
478
703
  }
479
- 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, }) {
480
705
  // A stage counts what it did and the queue is flushed after each new event. `record`
481
706
  // swallows its own failures, so this can neither fail a stage nor widen what leaves the
482
707
  // machine - the payload allow-list is checked inside it.
@@ -484,7 +709,17 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
484
709
  record(telemetry, type, counters);
485
710
  flusher.soon();
486
711
  };
487
- 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
+ };
488
723
  // A stage slice is handed the wrapped calls, never the registry: every provider call
489
724
  // it makes is already inside the retry policy (kernel/runner/providers.ts).
490
725
  const providers = {
@@ -492,12 +727,23 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
492
727
  attempts: sqliteAttempts(db, ids),
493
728
  clock,
494
729
  log,
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
+ }),
495
737
  queue: createProviderQueue((provider) => isLocalCliProvider(provider)
496
738
  ? localCliConcurrency(provider)
497
739
  : (catalogue.read().providers[provider]?.maxConcurrent ?? 1)),
498
740
  };
499
741
  const checkpoints = createCheckpointAuthority({
500
- 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
+ },
501
747
  approve: (projectId, checkpointId, identity) => approveCheckpoint(db, {
502
748
  ...identity,
503
749
  projectId,
@@ -518,20 +764,30 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
518
764
  },
519
765
  standingsOf: (projectId) => executionStandings(execution, projectId),
520
766
  ready: (work) => invocationReady(execution, work),
521
- 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),
522
771
  claim: (work) => {
523
772
  const claimed = claimWork(db, work);
524
773
  projectStandings(execution, work.projectId);
525
774
  return claimed;
526
775
  },
527
776
  maySubmit: (work, pieceId) => maySubmit(db, work, pieceId),
528
- 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) => {
529
784
  transact(db, () => {
530
- finishWork(db, work, state, reason);
785
+ finishWork(db, work, state, reason, kind ?? null);
531
786
  materializeAdmittedWork(execution, work.projectId);
532
787
  projectStandings(execution, work.projectId);
533
788
  settleReleasedCheckpoints(execution, work.projectId);
534
789
  });
790
+ onFinished?.(work);
535
791
  },
536
792
  },
537
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.