@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
@@ -0,0 +1,102 @@
1
+ import { z } from "zod";
2
+ import { readinessIsUsable } from "../../kernel/ports/model.js";
3
+ import { keyedProviders, readSetting, writeSetting } from "./repo.js";
4
+ // What a fresh Play form picks before the user picks anything. Written once, on the first
5
+ // launch that finds a command-line tool, and read by Play for every new draft.
6
+ const choiceSchema = z.object({ provider: z.string().min(1), model: z.string() }).strict();
7
+ export const providerDefaultsSchema = z
8
+ .object({ llm: choiceSchema.optional(), images: choiceSchema.optional() })
9
+ .strict();
10
+ export const providerDefaultsKey = "provider.defaults";
11
+ const defaultsKey = providerDefaultsKey;
12
+ const doneKey = "first-run.done";
13
+ // The order a text tool is preferred in when several are found.
14
+ const textPreference = ["claude-code", "codex", "gemini"];
15
+ export function readProviderDefaults(db) {
16
+ const raw = readSetting(db, defaultsKey);
17
+ if (raw === undefined)
18
+ return {};
19
+ try {
20
+ const parsed = providerDefaultsSchema.safeParse(JSON.parse(raw));
21
+ return parsed.success ? parsed.data : {};
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ export function dismissFirstRun(db) {
28
+ writeSetting(db, doneKey, "1");
29
+ }
30
+ // First launch: find Claude Code, Codex and Gemini, pick the found ones as Play's defaults, and
31
+ // say a video can be made without any API key. Later launches only report what was found.
32
+ export async function firstRunStatus(deps, statuses) {
33
+ const detected = statuses
34
+ .filter((status) => status.readiness.kind === "cli")
35
+ .map((status) => {
36
+ const readiness = status.readiness;
37
+ if (readiness.kind !== "cli")
38
+ throw new Error("Expected a command-line provider.");
39
+ return {
40
+ id: status.id,
41
+ displayName: status.id === "codex-image" ? "Codex CLI (images)" : status.displayName,
42
+ installed: readiness.installed,
43
+ usable: readinessIsUsable(readiness),
44
+ ...(readiness.version === undefined ? {} : { version: readiness.version }),
45
+ ...(readiness.issue === undefined ? {} : { issue: readiness.issue }),
46
+ };
47
+ });
48
+ const firstRun = keyedProviders(deps.db).size === 0 && readSetting(deps.db, doneKey) === undefined;
49
+ let defaults = readProviderDefaults(deps.db);
50
+ const usable = new Set(detected.filter((cli) => cli.usable).map((cli) => cli.id));
51
+ const text = textPreference.find((id) => usable.has(id));
52
+ if (firstRun &&
53
+ readSetting(deps.db, defaultsKey) === undefined &&
54
+ (text !== undefined || usable.has("codex-image"))) {
55
+ defaults = {
56
+ ...(text === undefined
57
+ ? {}
58
+ : { llm: { provider: text, model: await firstModel(deps, text, "llm") } }),
59
+ ...(usable.has("codex-image")
60
+ ? {
61
+ images: {
62
+ provider: "codex-image",
63
+ model: await firstModel(deps, "codex-image", "image"),
64
+ },
65
+ }
66
+ : {}),
67
+ };
68
+ writeSetting(deps.db, defaultsKey, JSON.stringify(defaults));
69
+ }
70
+ const names = detected
71
+ .filter((cli) => cli.usable && cli.id !== "codex-image")
72
+ .map((cli) => cli.displayName);
73
+ return {
74
+ firstRun,
75
+ detected,
76
+ defaults,
77
+ message: !firstRun
78
+ ? null
79
+ : names.length > 0
80
+ ? "You can make a video now, no API keys needed."
81
+ : "No AI command-line tool was found on this computer.",
82
+ detail: !firstRun
83
+ ? null
84
+ : names.length > 0
85
+ ? `Slopify found ${list(names)} and picked ${names.length === 1 ? "it" : "them"} on Play. Narration still needs a voice provider key, or turn Audio off or upload your own. Keys are optional extras in Settings → Providers.`
86
+ : "Install Claude Code, Codex or Gemini CLI and sign in once in a terminal, then reload this page. Or paste an API key in Settings → Providers.",
87
+ };
88
+ }
89
+ async function firstModel(deps, provider, family) {
90
+ try {
91
+ return (await deps.modelsFor?.(provider, family))?.[0]?.id ?? "";
92
+ }
93
+ catch {
94
+ // The picker shows the tool's list once it answers; the provider is still chosen.
95
+ return "";
96
+ }
97
+ }
98
+ function list(names) {
99
+ return names.length <= 1
100
+ ? (names[0] ?? "")
101
+ : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
102
+ }
@@ -0,0 +1,190 @@
1
+ import { choiceSites, retiredModelUsage, slotLabels, } from "../model-upkeep/usage.js";
2
+ import { cliPathStatus } from "./cli-paths.js";
3
+ import { keyGuides } from "./key-guides.js";
4
+ import { testProviderKey } from "./key-test.js";
5
+ import { isLocalCliProvider } from "./model.js";
6
+ import { providerStatuses } from "./readiness.js";
7
+ const signInCommand = {
8
+ "claude-code": "claude auth login",
9
+ codex: "codex login",
10
+ gemini: "gemini",
11
+ };
12
+ function loginSource(id) {
13
+ return id === "codex-image" ? "codex" : id;
14
+ }
15
+ // Where each provider's models are chosen, as "Text model in template “Weekly”".
16
+ function uses(db) {
17
+ const found = new Map();
18
+ for (const site of choiceSites(db))
19
+ for (const [slot, choice] of Object.entries(site.choices)) {
20
+ if (choice === undefined || choice.provider === "" || choice.model.trim() === "")
21
+ continue;
22
+ // Images drawn by Codex use the image provider id; the text choice uses the CLI's own.
23
+ const list = found.get(choice.provider) ?? [];
24
+ list.push({
25
+ model: choice.model,
26
+ where: `${slotLabels[slot]} in ${site.kind} “${site.name}”`,
27
+ });
28
+ found.set(choice.provider, list);
29
+ }
30
+ return found;
31
+ }
32
+ function overall(checks, used, setUp) {
33
+ if (checks.some((check) => check.state === "problem"))
34
+ return "problem";
35
+ if (checks.some((check) => check.state === "warning"))
36
+ return "warning";
37
+ return setUp || used ? "ok" : "unused";
38
+ }
39
+ // "Check all" on Settings → Providers: each command-line tool found and signed in, each saved
40
+ // key accepted by its provider, and each chosen model still offered.
41
+ export async function checkProviderHealth(deps) {
42
+ const statuses = await providerStatuses(deps);
43
+ const inUse = uses(deps.db);
44
+ const catalogue = deps.catalogue?.();
45
+ const retired = catalogue === undefined ? [] : retiredModelUsage(deps.db, catalogue);
46
+ const providers = await Promise.all(statuses.map((status) => status.readiness.kind === "cli"
47
+ ? cliHealth(deps, status, inUse.get(status.id) ?? [])
48
+ : keyedHealth(deps, status, inUse.get(status.id) ?? [], retired)));
49
+ return { checkedAt: deps.clock.now().toISOString(), providers };
50
+ }
51
+ async function cliHealth(deps, status, used) {
52
+ const { readiness } = status;
53
+ if (readiness.kind !== "cli" || !isLocalCliProvider(status.id))
54
+ throw new Error("Expected a command-line provider.");
55
+ const name = status.displayName;
56
+ const command = status.cliPath?.command ?? loginSource(status.id);
57
+ const checks = [];
58
+ const base = { id: status.id, displayName: name, family: status.family };
59
+ if (!readiness.installed ||
60
+ readiness.issueKind === "missing" ||
61
+ readiness.issueKind === "bridge") {
62
+ checks.push({
63
+ label: "Installed",
64
+ state: used.length > 0 ? "problem" : "skipped",
65
+ detail: readiness.issue ??
66
+ `${name} was not found on this computer. Install it, sign in once in a terminal, then choose Check all again. If it is installed somewhere unusual, set its path in Settings → Providers.`,
67
+ });
68
+ return { ...base, state: used.length > 0 ? "problem" : "unused", checks };
69
+ }
70
+ checks.push({
71
+ label: "Installed",
72
+ state: readiness.issueKind === "version" ? "problem" : "ok",
73
+ detail: readiness.issueKind === "version"
74
+ ? (readiness.issue ?? `Update ${name}, then choose Check all again.`)
75
+ : `Found ${command}${readiness.version === undefined ? "" : ` (version ${readiness.version})`}.`,
76
+ });
77
+ const source = loginSource(status.id);
78
+ let login = "unknown";
79
+ if (status.cliPath?.managedOnHost === true && deps.hostCliStatus !== undefined) {
80
+ login = (await deps.hostCliStatus(status.id)).login;
81
+ }
82
+ else if (deps.login !== undefined && source !== "gemini") {
83
+ try {
84
+ login = await deps.login(cliPathStatus(deps.db, status.id).command, source, AbortSignal.timeout(15_000));
85
+ }
86
+ catch {
87
+ login = "unknown";
88
+ }
89
+ }
90
+ checks.push(login === "signed-in"
91
+ ? { label: "Signed in", state: "ok", detail: `${name} is signed in.` }
92
+ : login === "signed-out"
93
+ ? {
94
+ label: "Signed in",
95
+ state: "problem",
96
+ detail: `${name} is not signed in. Open a terminal on the computer running it, run "${signInCommand[source]}" and sign in, then choose Check all again.`,
97
+ }
98
+ : {
99
+ label: "Signed in",
100
+ state: "skipped",
101
+ detail: source === "gemini"
102
+ ? `Gemini CLI has no sign-in check Slopify can ask. Run "gemini" in a terminal once to confirm it is signed in.`
103
+ : `Slopify could not tell whether ${name} is signed in. Run "${signInCommand[source]}" in a terminal if runs fail with a sign-in error.`,
104
+ });
105
+ if (used.length > 0)
106
+ checks.push(await cliModels(deps, status, used));
107
+ return { ...base, state: overall(checks, used.length > 0, true), checks };
108
+ }
109
+ async function cliModels(deps, status, used) {
110
+ if (deps.modelsFor === undefined)
111
+ return {
112
+ label: "Chosen models",
113
+ state: "skipped",
114
+ detail: "The model list is not loaded yet.",
115
+ };
116
+ let offered;
117
+ try {
118
+ offered = await deps.modelsFor(status.id, status.family);
119
+ }
120
+ catch {
121
+ return {
122
+ label: "Chosen models",
123
+ state: "warning",
124
+ detail: `Slopify could not read ${status.displayName}'s model list, so it could not check the chosen models. Make sure it is signed in and up to date, then choose Check all again.`,
125
+ };
126
+ }
127
+ const ids = new Set(offered.map((model) => model.id));
128
+ const missing = used.filter((use) => !ids.has(use.model));
129
+ if (missing.length === 0)
130
+ return {
131
+ label: "Chosen models",
132
+ state: "ok",
133
+ detail: `Every chosen model (${[...new Set(used.map((use) => use.model))].join(", ")}) is available.`,
134
+ };
135
+ return {
136
+ label: "Chosen models",
137
+ state: "problem",
138
+ detail: `${status.displayName} does not offer ${describe(missing)}. Update ${status.displayName}, or pick another model there and save.`,
139
+ };
140
+ }
141
+ function describe(missing) {
142
+ const shown = missing.slice(0, 3).map((use) => `${use.model} (${use.where})`);
143
+ return missing.length > 3
144
+ ? `${shown.join("; ")} and ${missing.length - 3} more`
145
+ : shown.join("; ");
146
+ }
147
+ async function keyedHealth(deps, status, used, retired) {
148
+ const { readiness } = status;
149
+ const base = { id: status.id, displayName: status.displayName, family: status.family };
150
+ const guide = keyGuides[status.id];
151
+ const checks = [];
152
+ const hasKey = readiness.kind === "keyed" && readiness.hasKey;
153
+ if (!hasKey) {
154
+ checks.push({
155
+ label: "Key saved",
156
+ state: used.length > 0 ? "problem" : "skipped",
157
+ detail: `No key is saved.${used.length > 0 ? ` It is chosen for ${describe(used)}.` : ""} Make one at ${guide?.keyPage.url ?? "the provider's website"}, paste it in Settings → Providers → ${status.displayName} and choose Save.`,
158
+ });
159
+ return { ...base, state: used.length > 0 ? "problem" : "unused", checks };
160
+ }
161
+ const test = await testProviderKey(deps, status.id);
162
+ checks.push({
163
+ label: "Key valid",
164
+ state: test.ok
165
+ ? "ok"
166
+ : test.result === "rate-limited" ||
167
+ test.result === "provider-down" ||
168
+ test.result === "unreachable"
169
+ ? "warning"
170
+ : "problem",
171
+ detail: test.message,
172
+ });
173
+ const mine = retired.filter((usage) => usage.provider === status.id);
174
+ if (mine.length > 0)
175
+ checks.push({
176
+ label: "Chosen models",
177
+ state: "problem",
178
+ detail: `${describe(mine.map((usage) => ({
179
+ model: usage.model,
180
+ where: `${slotLabels[usage.slot]} in ${usage.kind} “${usage.name}”`,
181
+ })))} ${mine.length === 1 ? "is" : "are"} no longer offered. Use Switch in Settings → Models → Retired models in use.`,
182
+ });
183
+ else if (used.length > 0)
184
+ checks.push({
185
+ label: "Chosen models",
186
+ state: "ok",
187
+ detail: "Every chosen model is still offered.",
188
+ });
189
+ return { ...base, state: overall(checks, used.length > 0, true), checks };
190
+ }
@@ -0,0 +1,128 @@
1
+ const openAi = {
2
+ signUp: { label: "platform.openai.com/signup", url: "https://platform.openai.com/signup" },
3
+ keyPage: { label: "platform.openai.com/api-keys", url: "https://platform.openai.com/api-keys" },
4
+ billing: {
5
+ label: "Billing",
6
+ url: "https://platform.openai.com/settings/organization/billing/overview",
7
+ },
8
+ steps: [
9
+ "Sign up or sign in to the OpenAI platform.",
10
+ "Open Billing and buy prepaid credit (new accounts need at least $5 before any call works).",
11
+ "Open API keys, choose Create new secret key, and copy it (it is shown once).",
12
+ "Paste it here, choose Save, then Test.",
13
+ ],
14
+ permissions: "A key with All permissions works. A restricted key needs Models: Read (for Test) and Model capabilities: Request (for speech and images). GPT Image models may also need Verify Organization under Settings → Organization → General on the OpenAI platform.",
15
+ docs: {
16
+ label: "OpenAI API docs",
17
+ url: "https://developers.openai.com/api/docs/guides/error-codes",
18
+ },
19
+ };
20
+ export const keyGuides = {
21
+ openrouter: {
22
+ signUp: { label: "openrouter.ai/sign-up", url: "https://openrouter.ai/sign-up" },
23
+ keyPage: { label: "openrouter.ai/settings/keys", url: "https://openrouter.ai/settings/keys" },
24
+ billing: { label: "Credits", url: "https://openrouter.ai/settings/credits" },
25
+ steps: [
26
+ "Sign up at OpenRouter.",
27
+ "Open Credits and buy credit (paid models are charged from it; a negative balance stops even free models).",
28
+ "Open Keys, choose Create API key, and copy it.",
29
+ "Paste it here, choose Save, then Test.",
30
+ ],
31
+ permissions: "Keys have no scopes. If you set a credit limit on the key, keep it above what a run costs.",
32
+ docs: { label: "OpenRouter docs", url: "https://openrouter.ai/docs/api-reference/limits" },
33
+ },
34
+ "openai-tts": openAi,
35
+ "openai-image": openAi,
36
+ "google-image": {
37
+ signUp: { label: "aistudio.google.com", url: "https://aistudio.google.com" },
38
+ keyPage: { label: "aistudio.google.com/apikey", url: "https://aistudio.google.com/apikey" },
39
+ billing: { label: "Projects → Set up billing", url: "https://aistudio.google.com/projects" },
40
+ steps: [
41
+ "Sign in to Google AI Studio with your Google account.",
42
+ "Open Projects and choose Set up billing next to your project: Gemini image models are not available on the free tier.",
43
+ "Open API keys, choose Create API key, and copy it.",
44
+ "Paste it here, choose Save, then Test.",
45
+ ],
46
+ permissions: "Use a key made in AI Studio. An older Google Cloud key must be set to Restrict to Gemini API only, or Google rejects it.",
47
+ docs: { label: "Gemini API keys", url: "https://ai.google.dev/gemini-api/docs/api-key" },
48
+ },
49
+ fal: {
50
+ signUp: { label: "fal.ai/login", url: "https://fal.ai/login" },
51
+ keyPage: { label: "fal.ai/dashboard/keys", url: "https://fal.ai/dashboard/keys" },
52
+ billing: { label: "Billing", url: "https://fal.ai/dashboard/billing" },
53
+ steps: [
54
+ "Sign up at fal.ai.",
55
+ "Open Billing and buy credit: fal.ai is prepaid and locks the account when the balance runs out.",
56
+ "Open Keys, choose Add key with the API scope, and copy it.",
57
+ "Paste it here, choose Save, then Test.",
58
+ ],
59
+ permissions: "The API scope is enough; Slopify does not need an ADMIN key.",
60
+ docs: {
61
+ label: "fal.ai authentication",
62
+ url: "https://fal.ai/docs/model-apis/authentication",
63
+ },
64
+ },
65
+ replicate: {
66
+ signUp: { label: "replicate.com/signin", url: "https://replicate.com/signin" },
67
+ keyPage: {
68
+ label: "replicate.com/account/api-tokens",
69
+ url: "https://replicate.com/account/api-tokens",
70
+ },
71
+ billing: { label: "Billing", url: "https://replicate.com/account/billing" },
72
+ steps: [
73
+ "Sign in to Replicate.",
74
+ "Open Billing and buy credit: Replicate needs credit up front before it runs paid models.",
75
+ "Open API tokens, create a token, and copy it (it starts with r8_).",
76
+ "Paste it here, choose Save, then Test.",
77
+ ],
78
+ permissions: "Tokens have no scopes.",
79
+ docs: { label: "Replicate billing", url: "https://replicate.com/docs/topics/billing" },
80
+ },
81
+ elevenlabs: {
82
+ signUp: { label: "elevenlabs.io/app/sign-up", url: "https://elevenlabs.io/app/sign-up" },
83
+ keyPage: {
84
+ label: "elevenlabs.io/app/developers/api-keys",
85
+ url: "https://elevenlabs.io/app/developers/api-keys",
86
+ },
87
+ steps: [
88
+ "Sign up at ElevenLabs.",
89
+ "Open Developers → API keys, choose Create key, and copy it.",
90
+ "Paste it here, choose Save, then Test.",
91
+ ],
92
+ permissions: "A restricted key needs Text to Speech access and Models read access (models_read) for Test; Voices read helps when picking voices. Remove any credit quota or IP allowlist that would block this computer.",
93
+ docs: {
94
+ label: "ElevenLabs API keys",
95
+ url: "https://elevenlabs.io/docs/api-reference/models/list",
96
+ },
97
+ },
98
+ cartesia: {
99
+ signUp: { label: "play.cartesia.ai/sign-up", url: "https://play.cartesia.ai/sign-up" },
100
+ keyPage: { label: "play.cartesia.ai/keys", url: "https://play.cartesia.ai/keys" },
101
+ steps: [
102
+ "Sign up at Cartesia (the free plan needs no card).",
103
+ "Open API Keys, create a key, and copy it (it starts with sk_car_).",
104
+ "Paste it here, choose Save, then Test.",
105
+ ],
106
+ permissions: "Keys have no scopes.",
107
+ docs: {
108
+ label: "Cartesia authentication",
109
+ url: "https://docs.cartesia.ai/get-started/authenticate-your-client-applications",
110
+ },
111
+ },
112
+ inworld: {
113
+ signUp: { label: "platform.inworld.ai/signup", url: "https://platform.inworld.ai/signup" },
114
+ keyPage: {
115
+ label: "platform.inworld.ai/api-keys",
116
+ url: "https://platform.inworld.ai/api-keys",
117
+ },
118
+ billing: { label: "Billing", url: "https://platform.inworld.ai/billing" },
119
+ steps: [
120
+ "Sign up at the Inworld platform.",
121
+ "Open API Keys and choose Generate new key (a Standard key, not Realtime-only).",
122
+ "Copy the Base64 credentials value exactly as shown, without encoding it again.",
123
+ "Paste it here, choose Save, then Test.",
124
+ ],
125
+ permissions: "A Standard key covers speech and reading voices. Realtime-only keys do not work.",
126
+ docs: { label: "Inworld API keys", url: "https://docs.inworld.ai/portal/api-keys" },
127
+ },
128
+ };
@@ -0,0 +1,85 @@
1
+ import { keyGuides } from "./key-guides.js";
2
+ import { providerById } from "./model.js";
3
+ import { keyOf } from "./repo.js";
4
+ // What the Test button found. `ok` is the only thing the screen needs to colour the line;
5
+ // `message` says what happened and, when it failed, exactly what to do.
6
+ export const keyTestResults = [
7
+ "valid",
8
+ "no-key",
9
+ "rejected",
10
+ "forbidden",
11
+ "no-credit",
12
+ "rate-limited",
13
+ "provider-down",
14
+ "unreachable",
15
+ "unexpected",
16
+ ];
17
+ // The mapping from what came back to what the user reads. Pure, so every branch is tested
18
+ // without a network.
19
+ export function keyTestOutcome(provider, answer, checkedAt, badKey) {
20
+ const name = providerById(provider).displayName;
21
+ const guide = keyGuides[provider];
22
+ const keyPage = guide === undefined ? "the provider's website" : guide.keyPage.url;
23
+ const billing = guide?.billing?.url;
24
+ const place = `Settings → Providers → ${name}`;
25
+ const done = (result, message) => ({
26
+ provider,
27
+ result,
28
+ ok: result === "valid",
29
+ message,
30
+ checkedAt,
31
+ });
32
+ if ("failure" in answer) {
33
+ if (answer.failure === "no-key")
34
+ return done("no-key", `No ${name} key is saved yet. Make one at ${keyPage}, paste it in ${place} and choose Save, then Test.`);
35
+ return done("unreachable", answer.failure === "timeout"
36
+ ? `${name} did not answer within 15 seconds, so the key could not be checked. Check your internet connection, then choose Test again.`
37
+ : `Slopify could not reach ${name}, so the key could not be checked. Check your internet connection (and any firewall or proxy), then choose Test again.`);
38
+ }
39
+ const { status } = answer;
40
+ if (status >= 200 && status < 300)
41
+ return done("valid", provider === "openrouter"
42
+ ? `${name} accepted this key.`
43
+ : `${name} accepted this key. The account's credit is only checked when Slopify first generates with it.`);
44
+ if (status === 401 || badKey?.(status, answer.body ?? "") === true)
45
+ return done("rejected", `${name} did not accept this key (HTTP ${status}): it is mistyped, revoked or for another account. Make a new key at ${keyPage}, paste it in ${place}, choose Save, then Test again.`);
46
+ if (status === 402)
47
+ return done("no-credit", `${name} accepted the key but the account has no credit or payment method (HTTP 402). Add credit${billing === undefined ? " in your account" : ` at ${billing}`}, then choose Test again.`);
48
+ if (status === 403)
49
+ return done("forbidden", `${name} recognised the key but refused this request (HTTP 403). The key may be restricted, or the account may need verifying or billing set up. ${guide?.permissions ?? ""} Fix it at ${keyPage}, then choose Test again.`.replace(/\s+/g, " "));
50
+ if (status === 429)
51
+ return done("rate-limited", `${name} said "too many requests" (HTTP 429), so the key could not be checked just now. It is usually valid; wait a minute and choose Test again. If it keeps happening, check the account's limits and credit${billing === undefined ? "" : ` at ${billing}`}.`);
52
+ if (status >= 500)
53
+ return done("provider-down", `${name} had a problem on its side (HTTP ${status}), so the key could not be checked. Wait a few minutes and choose Test again.`);
54
+ return done("unexpected", `${name} answered the key check with HTTP ${status}, which Slopify did not expect. Check the key at ${keyPage}, then choose Test again; if it keeps happening, use Download diagnostics in Settings and report it.`);
55
+ }
56
+ // The Test button. The key is read for this one request, sent only to its own provider, and
57
+ // never appears in the answer, a log line or an error.
58
+ export async function testProviderKey(deps, provider) {
59
+ const at = () => deps.clock.now().toISOString();
60
+ const probe = Object.hasOwn(deps.probes, provider) ? deps.probes[provider] : undefined;
61
+ if (probe === undefined || providerById(provider).auth !== "key")
62
+ throw new Error(`${provider} has no key to test`);
63
+ const key = keyOf(deps.db, provider);
64
+ if (key === undefined)
65
+ return keyTestOutcome(provider, { failure: "no-key" }, at());
66
+ let status;
67
+ let body = "";
68
+ try {
69
+ const response = await deps.fetch(probe.url, {
70
+ headers: probe.headers(key),
71
+ signal: AbortSignal.timeout(15_000),
72
+ redirect: "error",
73
+ });
74
+ status = response.status;
75
+ // Only an error body is read, and only its start: enough for the provider's error code.
76
+ body = status >= 400 ? (await response.text()).slice(0, 4096) : "";
77
+ if (status < 400)
78
+ await response.body?.cancel().catch(() => { });
79
+ }
80
+ catch (error) {
81
+ const timeout = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
82
+ return keyTestOutcome(provider, { failure: timeout ? "timeout" : "network" }, at());
83
+ }
84
+ return keyTestOutcome(provider, { status, body }, at(), probe.badKey);
85
+ }
@@ -1,4 +1,5 @@
1
1
  // The provider catalogue and the settings domain types.
2
+ import { defaultLoudness } from "../loudness/model.js";
2
3
  // The three families are the three ports, so the set is named beside them.
3
4
  export { providerFamilies } from "../../kernel/ports/model.js";
4
5
  // The supported set. OpenAI ships two adapters with two ids, one per family:
@@ -22,6 +23,11 @@ export const providerIds = [
22
23
  export function isLocalCliProvider(id) {
23
24
  return id === "claude-code" || id === "codex" || id === "gemini" || id === "codex-image";
24
25
  }
26
+ // How many calls to a command-line tool run at once. Each Codex image is its own job - its
27
+ // own folder and thread - so four are drawn side by side; the text CLIs keep three.
28
+ export function localCliConcurrency(id) {
29
+ return id === "codex-image" ? 4 : 3;
30
+ }
25
31
  export const providers = [
26
32
  { id: "openrouter", family: "llm", displayName: "OpenRouter", auth: "key" },
27
33
  {
@@ -76,4 +82,8 @@ export function providerById(id) {
76
82
  }
77
83
  // The theme override the Appearance control writes.
78
84
  export const appearances = ["system", "light", "dark"];
79
- export const defaultSettings = { silenceGapSeconds: 3, appearance: "system" };
85
+ export const defaultSettings = {
86
+ silenceGapSeconds: 3,
87
+ appearance: "system",
88
+ loudness: defaultLoudness,
89
+ };
@@ -1,17 +1,25 @@
1
1
  import { z } from "zod";
2
2
  import { silenceGapSecondsMax } from "../admission/rules.js";
3
+ import { loudnessFields } from "../loudness/model.js";
3
4
  import { appearances, defaultSettings } from "./model.js";
4
5
  import { readSetting, writeSetting } from "./repo.js";
5
6
  // The `settings` table is key/value, so each setting is its own row and a value the app
6
7
  // does not understand costs only that one setting.
7
8
  const silenceGapKey = "silenceGapSeconds";
8
9
  const appearanceKey = "appearance";
10
+ const loudnessKey = "loudness";
9
11
  const silenceGapValue = z.number().int().min(0).max(silenceGapSecondsMax);
10
12
  const appearanceValue = z.enum(appearances);
13
+ const loudnessValue = z.object({
14
+ enabled: z.boolean(),
15
+ videoLufs: z.number(),
16
+ audioFilesLufs: z.number(),
17
+ });
11
18
  export function readSettings(deps) {
12
19
  return {
13
20
  silenceGapSeconds: parsed(deps, silenceGapKey, silenceGapValue, defaultSettings.silenceGapSeconds),
14
21
  appearance: parsed(deps, appearanceKey, appearanceValue, defaultSettings.appearance),
22
+ loudness: parsed(deps, loudnessKey, loudnessValue, defaultSettings.loudness),
15
23
  };
16
24
  }
17
25
  export function saveSettings(deps, settings) {
@@ -25,11 +33,18 @@ export function saveSettings(deps, settings) {
25
33
  if (!appearanceValue.safeParse(settings.appearance).success) {
26
34
  fields.push({ field: "appearance", message: "Choose System, Light or Dark." });
27
35
  }
36
+ if (!loudnessValue.safeParse(settings.loudness).success) {
37
+ fields.push({ field: "loudness", message: "Choose whether new runs level the volume." });
38
+ }
39
+ else {
40
+ fields.push(...loudnessFields(settings.loudness));
41
+ }
28
42
  if (fields.length > 0) {
29
43
  return { ok: false, fields };
30
44
  }
31
45
  writeSetting(deps.db, silenceGapKey, JSON.stringify(settings.silenceGapSeconds));
32
46
  writeSetting(deps.db, appearanceKey, JSON.stringify(settings.appearance));
47
+ writeSetting(deps.db, loudnessKey, JSON.stringify(settings.loudness));
33
48
  return { ok: true, settings };
34
49
  }
35
50
  // A stored value is JSON, and a hand-edited database is the only way it can be anything
@@ -10,7 +10,9 @@ const voiceRow = z.object({
10
10
  provider: z.enum(providerIds),
11
11
  name: z.string(),
12
12
  voice_id: z.string(),
13
+ languages_json: z.string().nullable(),
13
14
  });
15
+ const languagesJson = z.array(z.string());
14
16
  const settingRow = z.object({ value: z.string() });
15
17
  export function upsertKey(db, provider, key, updatedAt) {
16
18
  db.prepare("INSERT INTO provider_keys (provider,key,updated_at,credential_generation) VALUES (?,?,?,?)" +
@@ -37,11 +39,16 @@ export function hasKey(db, provider) {
37
39
  return db.prepare("SELECT 1 FROM provider_keys WHERE provider = ?").get(provider) !== undefined;
38
40
  }
39
41
  export function insertVoice(db, voice) {
40
- db.prepare("INSERT INTO voices (id, provider, name, voice_id) VALUES (?, ?, ?, ?)").run(voice.id, voice.provider, voice.name, voice.voiceId);
42
+ db.prepare("INSERT INTO voices (id, provider, name, voice_id, languages_json) VALUES (?, ?, ?, ?, ?)").run(voice.id, voice.provider, voice.name, voice.voiceId, voice.languages === undefined ? null : JSON.stringify(voice.languages));
43
+ }
44
+ export function updateVoiceLanguages(db, id, languages) {
45
+ return (Number(db
46
+ .prepare("UPDATE voices SET languages_json = ? WHERE id = ?")
47
+ .run(languages === undefined ? null : JSON.stringify(languages), id).changes) > 0);
41
48
  }
42
49
  export function listVoices(db) {
43
50
  return db
44
- .prepare("SELECT id, provider, name, voice_id FROM voices ORDER BY provider, name, voice_id")
51
+ .prepare("SELECT id, provider, name, voice_id, languages_json FROM voices ORDER BY provider, name, voice_id")
45
52
  .all()
46
53
  .map((row) => toVoice(voiceRow.parse(row)));
47
54
  }
@@ -57,5 +64,12 @@ export function writeSetting(db, key, value) {
57
64
  " ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
58
65
  }
59
66
  function toVoice(row) {
60
- return { id: row.id, provider: row.provider, name: row.name, voiceId: row.voice_id };
67
+ const languages = row.languages_json === null ? undefined : languagesJson.parse(JSON.parse(row.languages_json));
68
+ return {
69
+ id: row.id,
70
+ provider: row.provider,
71
+ name: row.name,
72
+ voiceId: row.voice_id,
73
+ ...(languages === undefined ? {} : { languages }),
74
+ };
61
75
  }