@gentbajko/slopify 4.0.0 → 4.1.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 (366) hide show
  1. package/dist/adapters/account-limits.js +1 -1
  2. package/dist/adapters/alignment/audio.js +1 -1
  3. package/dist/adapters/alignment/cache.js +1 -1
  4. package/dist/adapters/alignment/multilingual.js +2 -1
  5. package/dist/adapters/alignment/runner.js +1 -1
  6. package/dist/adapters/alignment/worker.js +1 -1
  7. package/dist/adapters/explain.js +10 -9
  8. package/dist/adapters/host-cli/index.js +2 -1
  9. package/dist/adapters/host-cli/transport.js +2 -1
  10. package/dist/adapters/image/bytes.js +2 -1
  11. package/dist/adapters/image/codex.js +6 -5
  12. package/dist/adapters/image/fal.js +4 -3
  13. package/dist/adapters/image/google.js +3 -2
  14. package/dist/adapters/image/models.js +2 -2
  15. package/dist/adapters/image/openai.js +2 -1
  16. package/dist/adapters/image/replicate.js +3 -2
  17. package/dist/adapters/image/video.js +2 -1
  18. package/dist/adapters/key-probes.js +1 -1
  19. package/dist/adapters/llm/antigravity-models.js +1 -1
  20. package/dist/adapters/llm/antigravity.js +7 -6
  21. package/dist/adapters/llm/assistant-cli.js +1 -1
  22. package/dist/adapters/llm/claude-code-models.js +1 -1
  23. package/dist/adapters/llm/claude-code.js +3 -2
  24. package/dist/adapters/llm/codex-models.js +1 -1
  25. package/dist/adapters/llm/document-workspace.js +3 -2
  26. package/dist/adapters/llm/image-workspace.js +3 -2
  27. package/dist/adapters/llm/openrouter.js +2 -1
  28. package/dist/adapters/llm/run-cli.js +3 -2
  29. package/dist/adapters/tts/gemini.js +2 -1
  30. package/dist/adapters/tts/inworld-async.js +2 -1
  31. package/dist/adapters/tts/inworld.js +3 -2
  32. package/dist/adapters/tts/system.js +2 -1
  33. package/dist/catalog/registry.js +20 -13
  34. package/dist/catalog/schema.js +2 -1
  35. package/dist/catalog/validate.js +5 -2
  36. package/dist/edge/http/actions.js +7 -7
  37. package/dist/edge/http/app.js +3 -3
  38. package/dist/edge/http/assistant.js +3 -1
  39. package/dist/edge/http/backups.js +1 -1
  40. package/dist/edge/http/channel-memory.js +2 -2
  41. package/dist/edge/http/channels.js +1 -1
  42. package/dist/edge/http/checkpoints.js +7 -5
  43. package/dist/edge/http/design-templates.js +3 -3
  44. package/dist/edge/http/designs.js +1 -1
  45. package/dist/edge/http/document-themes.js +3 -3
  46. package/dist/edge/http/draft-problem.js +1 -1
  47. package/dist/edge/http/extension-auth.js +1 -1
  48. package/dist/edge/http/home.js +9 -1
  49. package/dist/edge/http/model-checks.js +4 -4
  50. package/dist/edge/http/onboarding.js +29 -12
  51. package/dist/edge/http/outputs.js +3 -2
  52. package/dist/edge/http/patch-notes.js +2 -2
  53. package/dist/edge/http/planning.js +1 -1
  54. package/dist/edge/http/project-create.js +1 -1
  55. package/dist/edge/http/project-templates.js +2 -2
  56. package/dist/edge/http/projects.js +22 -4
  57. package/dist/edge/http/prompts.js +2 -2
  58. package/dist/edge/http/providers.js +9 -10
  59. package/dist/edge/http/refusal.js +1 -1
  60. package/dist/edge/http/remote-access.js +4 -4
  61. package/dist/edge/http/remote-guard.js +2 -2
  62. package/dist/edge/http/reviews.js +1 -1
  63. package/dist/edge/http/revision-files.js +1 -1
  64. package/dist/edge/http/revisions.js +4 -4
  65. package/dist/edge/http/schedules.js +13 -11
  66. package/dist/edge/http/settings.js +6 -2
  67. package/dist/edge/http/social.js +5 -4
  68. package/dist/edge/http/staging.js +1 -1
  69. package/dist/edge/http/storage.js +12 -4
  70. package/dist/edge/http/studio.js +71 -19
  71. package/dist/edge/http/style-preview.js +2 -2
  72. package/dist/edge/http/template-studio.js +12 -9
  73. package/dist/edge/http/trash.js +1 -1
  74. package/dist/edge/http/youtube-edits.js +1 -1
  75. package/dist/edge/mcp/excluded-routes.js +4 -3
  76. package/dist/edge/mcp/instructions.js +3 -3
  77. package/dist/edge/mcp/library-tools.js +8 -5
  78. package/dist/edge/mcp/remake-tools.js +7 -4
  79. package/dist/edge/mcp/server.js +1 -1
  80. package/dist/edge/mcp/tools-channels.js +9 -9
  81. package/dist/edge/mcp/tools-designs.js +9 -9
  82. package/dist/edge/mcp/tools-library.js +7 -7
  83. package/dist/edge/mcp/tools-planning.js +18 -18
  84. package/dist/edge/mcp/tools-projects.js +10 -10
  85. package/dist/edge/mcp/tools-publishing.js +9 -9
  86. package/dist/edge/mcp/tools-runs.js +9 -9
  87. package/dist/edge/mcp/tools-schedules.js +6 -6
  88. package/dist/edge/mcp/tools-setup.js +20 -14
  89. package/dist/edge/mcp/tools-social.js +5 -4
  90. package/dist/edge/mcp/tools-storage.js +3 -3
  91. package/dist/edge/mcp/tools-template-studio.js +18 -18
  92. package/dist/edge/remote-access/model.js +1 -1
  93. package/dist/extension/slopify-studio-chrome.zip +0 -0
  94. package/dist/extension/slopify-studio-firefox.zip +0 -0
  95. package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
  96. package/dist/kernel/pipeline.js +12 -0
  97. package/dist/kernel/ports/settings-places.js +31 -0
  98. package/dist/kernel/ports/system-speech.js +31 -3
  99. package/dist/kernel/runner/attempt.js +11 -3
  100. package/dist/kernel/runner/providers.js +3 -2
  101. package/dist/kernel/runner/standalone.js +2 -2
  102. package/dist/main.js +5 -3
  103. package/dist/model-catalog.js +20 -0
  104. package/dist/patch-notes/4.0.0.md +13 -13
  105. package/dist/patch-notes/4.1.0.md +42 -0
  106. package/dist/patch-notes/index.json +6 -0
  107. package/dist/slices/admission/listing-facts.js +95 -0
  108. package/dist/slices/admission/repo.js +9 -6
  109. package/dist/slices/admission/rules.js +7 -7
  110. package/dist/slices/admission/state-words.js +34 -5
  111. package/dist/slices/assistant/cost.js +1 -1
  112. package/dist/slices/assistant/model.js +10 -3
  113. package/dist/slices/assistant/turn.js +5 -3
  114. package/dist/slices/autopilot/warnings.js +1 -1
  115. package/dist/slices/backups/service.js +5 -5
  116. package/dist/slices/channels/cast-images.js +1 -1
  117. package/dist/slices/channels/schema.js +1 -1
  118. package/dist/slices/channels/videos.js +1 -1
  119. package/dist/slices/control/providers.js +2 -2
  120. package/dist/slices/designs/budget.js +1 -1
  121. package/dist/slices/designs/calls.js +2 -2
  122. package/dist/slices/designs/check-context.js +4 -4
  123. package/dist/slices/designs/fields.js +4 -4
  124. package/dist/slices/designs/gate.js +7 -5
  125. package/dist/slices/designs/render-input.js +1 -1
  126. package/dist/slices/designs/requests.js +1 -1
  127. package/dist/slices/designs/runner.js +1 -1
  128. package/dist/slices/designs/sample.js +2 -1
  129. package/dist/slices/designs/service.js +1 -1
  130. package/dist/slices/designs/template-checks.js +2 -2
  131. package/dist/slices/designs/template-make.js +3 -3
  132. package/dist/slices/designs/templates.js +3 -3
  133. package/dist/slices/document/fonts.js +1 -1
  134. package/dist/slices/document/model.js +7 -4
  135. package/dist/slices/document/render.js +2 -1
  136. package/dist/slices/document/theme-schema.js +1 -1
  137. package/dist/slices/document/theme.js +49 -3
  138. package/dist/slices/episodes/summarize.js +2 -2
  139. package/dist/slices/estimate/index.js +28 -22
  140. package/dist/slices/estimate/requests.js +4 -4
  141. package/dist/slices/fixes/rules.js +3 -1
  142. package/dist/slices/fonts/catalog.js +2 -1
  143. package/dist/slices/fonts/coverage.js +2 -1
  144. package/dist/slices/images/appearance.js +2 -1
  145. package/dist/slices/images/scenes.js +2 -1
  146. package/dist/slices/keywords/digest.js +2 -1
  147. package/dist/slices/keywords/rules.js +2 -2
  148. package/dist/slices/keywords/specs.js +5 -5
  149. package/dist/slices/keywords/values.js +1 -1
  150. package/dist/slices/library/transfer.js +3 -3
  151. package/dist/slices/loudness/loudnorm.js +2 -1
  152. package/dist/slices/loudness/model.js +3 -2
  153. package/dist/slices/loudness/sentence-level.js +2 -1
  154. package/dist/slices/model-checks/rules.js +13 -0
  155. package/dist/slices/model-upkeep/model.js +1 -1
  156. package/dist/slices/model-upkeep/switch.js +3 -3
  157. package/dist/slices/model-upkeep/usage.js +13 -2
  158. package/dist/slices/narration/breaths.js +2 -1
  159. package/dist/slices/narration/clean-pieces.js +2 -1
  160. package/dist/slices/narration/pauses-model.js +3 -2
  161. package/dist/slices/narration/pronunciation.js +3 -2
  162. package/dist/slices/notifications/subject.js +2 -2
  163. package/dist/slices/onboarding/copy.js +19 -2
  164. package/dist/slices/onboarding/first-run.js +13 -38
  165. package/dist/slices/onboarding/install.js +2 -1
  166. package/dist/slices/onboarding/model.js +26 -21
  167. package/dist/slices/onboarding/preferred-model.js +14 -0
  168. package/dist/slices/onboarding/quick-short.js +103 -64
  169. package/dist/slices/onboarding/sample.js +4 -1
  170. package/dist/slices/onboarding/state.js +7 -0
  171. package/dist/slices/onboarding/template.js +6 -5
  172. package/dist/slices/outputs/add.js +3 -2
  173. package/dist/slices/outputs/service.js +2 -2
  174. package/dist/slices/patch-notes/library.js +1 -1
  175. package/dist/slices/play-drafts/readiness.js +3 -3
  176. package/dist/slices/play-drafts/review-inputs.js +1 -1
  177. package/dist/slices/play-drafts/service.js +17 -7
  178. package/dist/slices/project-templates/next-chapter.js +2 -1
  179. package/dist/slices/project-templates/provider-defaults.js +22 -0
  180. package/dist/slices/project-templates/setup-sections.js +26 -0
  181. package/dist/slices/project-templates/setup.js +6 -1
  182. package/dist/slices/rebuild/preview-details.js +5 -5
  183. package/dist/slices/rebuild/recipe-preparation.js +1 -1
  184. package/dist/slices/rebuild/recipe-save.js +2 -2
  185. package/dist/slices/rebuild/recipe-validation.js +5 -4
  186. package/dist/slices/rebuild/recipe-voices.js +3 -2
  187. package/dist/slices/rebuild/recipe-work.js +4 -4
  188. package/dist/slices/rebuild/recovery.js +6 -6
  189. package/dist/slices/rebuild/remake-outdated.js +5 -5
  190. package/dist/slices/rebuild/review-redo.js +1 -1
  191. package/dist/slices/rebuild/runtime-animate.js +2 -2
  192. package/dist/slices/rebuild/runtime-article.js +4 -3
  193. package/dist/slices/rebuild/runtime-document.js +5 -4
  194. package/dist/slices/rebuild/runtime-export-bed.js +2 -1
  195. package/dist/slices/rebuild/runtime-export-edit.js +2 -1
  196. package/dist/slices/rebuild/runtime-export-inputs.js +5 -4
  197. package/dist/slices/rebuild/runtime-export-short.js +1 -1
  198. package/dist/slices/rebuild/runtime-export.js +4 -3
  199. package/dist/slices/rebuild/runtime-image.js +4 -3
  200. package/dist/slices/rebuild/runtime-local.js +3 -2
  201. package/dist/slices/rebuild/runtime-narration-text.js +3 -2
  202. package/dist/slices/rebuild/runtime-provider.js +7 -6
  203. package/dist/slices/rebuild/runtime-review.js +6 -5
  204. package/dist/slices/rebuild/runtime-shorts.js +6 -5
  205. package/dist/slices/rebuild/runtime-store.js +17 -2
  206. package/dist/slices/rebuild/runtime-subtitles.js +13 -12
  207. package/dist/slices/rebuild/runtime-voices.js +2 -1
  208. package/dist/slices/rebuild/runtime-youtube.js +3 -2
  209. package/dist/slices/rebuild/service-readiness.js +11 -3
  210. package/dist/slices/research/synthesis.js +3 -2
  211. package/dist/slices/reviews/external-model.js +1 -1
  212. package/dist/slices/reviews/holds.js +5 -4
  213. package/dist/slices/reviews/model.js +8 -5
  214. package/dist/slices/reviews/rules.js +3 -3
  215. package/dist/slices/reviews/verdict.js +2 -1
  216. package/dist/slices/revisions/image-plan.js +1 -1
  217. package/dist/slices/revisions/mutation-assets.js +2 -1
  218. package/dist/slices/revisions/mutations.js +3 -2
  219. package/dist/slices/schedules/agenda.js +7 -5
  220. package/dist/slices/schedules/design-check.js +4 -4
  221. package/dist/slices/schedules/design-run.js +3 -3
  222. package/dist/slices/schedules/repo.js +6 -5
  223. package/dist/slices/schedules/schema.js +3 -0
  224. package/dist/slices/schedules/topics.js +5 -5
  225. package/dist/slices/settings/first-run.js +6 -32
  226. package/dist/slices/settings/health.js +30 -15
  227. package/dist/slices/settings/key-test.js +1 -1
  228. package/dist/slices/settings/model.js +4 -2
  229. package/dist/slices/settings/playback.js +2 -2
  230. package/dist/slices/shorts/keep.js +2 -1
  231. package/dist/slices/shorts/pick.js +4 -3
  232. package/dist/slices/shorts/prompts.js +2 -1
  233. package/dist/slices/shorts/render.js +3 -2
  234. package/dist/slices/social/media.js +5 -4
  235. package/dist/slices/social/pack.js +1 -1
  236. package/dist/slices/social/post-prompt.js +2 -1
  237. package/dist/slices/social/reshape.js +2 -2
  238. package/dist/slices/social/serve.js +1 -1
  239. package/dist/slices/social/service.js +3 -2
  240. package/dist/slices/social/state.js +1 -1
  241. package/dist/slices/storage/backup-import.js +2 -2
  242. package/dist/slices/storage/portable.js +1 -1
  243. package/dist/slices/storage/trim.js +23 -13
  244. package/dist/slices/studio/calendar.js +3 -1
  245. package/dist/slices/studio/disclosure.js +4 -4
  246. package/dist/slices/studio/model.js +1 -1
  247. package/dist/slices/studio/pack.js +9 -8
  248. package/dist/slices/studio/plan.js +1 -1
  249. package/dist/slices/studio/settings.js +6 -6
  250. package/dist/slices/studio/videos.js +23 -4
  251. package/dist/slices/style-preview/render.js +3 -3
  252. package/dist/slices/style-preview/schema.js +1 -1
  253. package/dist/slices/subtitles/captions.js +1 -1
  254. package/dist/slices/template-studio/anatomy-view.js +46 -3
  255. package/dist/slices/template-studio/bakeoff.js +3 -3
  256. package/dist/slices/template-studio/budget.js +6 -3
  257. package/dist/slices/template-studio/cards.js +78 -0
  258. package/dist/slices/template-studio/checks.js +99 -50
  259. package/dist/slices/template-studio/for-all.js +5 -5
  260. package/dist/slices/template-studio/free-video.js +1 -1
  261. package/dist/slices/template-studio/free.js +5 -5
  262. package/dist/slices/template-studio/model.js +19 -15
  263. package/dist/slices/template-studio/pack-import.js +14 -14
  264. package/dist/slices/template-studio/pack.js +9 -7
  265. package/dist/slices/template-studio/paid.js +12 -5
  266. package/dist/slices/template-studio/quote.js +18 -11
  267. package/dist/slices/template-studio/ready.js +7 -7
  268. package/dist/slices/template-studio/runner.js +1 -1
  269. package/dist/slices/template-studio/save.js +12 -12
  270. package/dist/slices/template-studio/say-it-as.js +3 -3
  271. package/dist/slices/template-studio/schedule-gate.js +1 -1
  272. package/dist/slices/template-studio/schedule-price.js +3 -1
  273. package/dist/slices/template-studio/schedule-sample.js +5 -1
  274. package/dist/slices/template-studio/schema.js +25 -0
  275. package/dist/slices/template-studio/service.js +3 -1
  276. package/dist/slices/template-studio/status.js +26 -9
  277. package/dist/slices/template-studio/topic.js +3 -3
  278. package/dist/slices/thumbnail/by-llm.js +1 -1
  279. package/dist/slices/uploads/repo.js +37 -1
  280. package/dist/slices/uploads/rules.js +3 -2
  281. package/dist/slices/video/edit-list.js +2 -2
  282. package/dist/slices/video/edit-settings.js +1 -1
  283. package/dist/slices/video/encoding.js +14 -5
  284. package/dist/slices/video/ffmpeg.js +2 -2
  285. package/dist/slices/video/plan.js +2 -1
  286. package/dist/slices/video/slideshow.js +1 -1
  287. package/dist/slices/voices/attribution.js +3 -2
  288. package/dist/slices/voices/model.js +9 -8
  289. package/dist/slices/voices/script.js +2 -1
  290. package/dist/slices/youtube/answer.js +2 -1
  291. package/dist/slices/youtube/edits-repo.js +1 -1
  292. package/dist/tutorials/AI-CLIs.md +25 -25
  293. package/dist/tutorials/Agents-MCP.md +32 -32
  294. package/dist/tutorials/Assistant.md +5 -5
  295. package/dist/tutorials/Backups.md +7 -7
  296. package/dist/tutorials/Calendar.md +17 -25
  297. package/dist/tutorials/Cast-Library.md +19 -19
  298. package/dist/tutorials/Channels.md +40 -35
  299. package/dist/tutorials/Costs-and-Run-Cost.md +38 -40
  300. package/dist/tutorials/Create-Article.md +114 -0
  301. package/dist/tutorials/Create-Extras.md +112 -0
  302. package/dist/tutorials/Create-Images.md +165 -0
  303. package/dist/tutorials/Create-Narration.md +178 -0
  304. package/dist/tutorials/Create-Overview.md +244 -0
  305. package/dist/tutorials/Create-Video-and-Style.md +144 -0
  306. package/dist/tutorials/Docker.md +3 -3
  307. package/dist/tutorials/Document-Themes.md +32 -32
  308. package/dist/tutorials/Editing-a-Project.md +94 -94
  309. package/dist/tutorials/FAQ.md +18 -18
  310. package/dist/tutorials/First-Launch-and-Welcome.md +50 -64
  311. package/dist/tutorials/Home-and-Projects.md +101 -76
  312. package/dist/tutorials/Home.md +33 -33
  313. package/dist/tutorials/Install.md +13 -13
  314. package/dist/tutorials/Intros-and-Outros.md +33 -34
  315. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +62 -58
  316. package/dist/tutorials/Keywords-Slopify-Fills.md +21 -21
  317. package/dist/tutorials/Models.md +27 -27
  318. package/dist/tutorials/Multiple-Voices.md +41 -40
  319. package/dist/tutorials/Narration-Aliases-and-Glossary.md +14 -14
  320. package/dist/tutorials/Notifications.md +21 -17
  321. package/dist/tutorials/Other-Languages.md +15 -15
  322. package/dist/tutorials/PDF-Documents.md +38 -29
  323. package/dist/tutorials/Posting-to-Social-Sites.md +12 -12
  324. package/dist/tutorials/Project-Page.md +118 -120
  325. package/dist/tutorials/Prompts-and-Templates.md +84 -0
  326. package/dist/tutorials/Prompts.md +45 -45
  327. package/dist/tutorials/Providers-and-Keys.md +105 -98
  328. package/dist/tutorials/Publishing-to-YouTube.md +31 -31
  329. package/dist/tutorials/Recovery-and-Retries.md +31 -31
  330. package/dist/tutorials/Remote-Access.md +2 -2
  331. package/dist/tutorials/Reviews-and-Checkpoints.md +90 -93
  332. package/dist/tutorials/Schedules.md +27 -28
  333. package/dist/tutorials/Settings-Reference.md +182 -204
  334. package/dist/tutorials/Shorts.md +34 -33
  335. package/dist/tutorials/Start-at-Login.md +3 -3
  336. package/dist/tutorials/Studio-Extension.md +22 -24
  337. package/dist/tutorials/Template-Studio.md +45 -42
  338. package/dist/tutorials/Templates.md +50 -79
  339. package/dist/tutorials/Trash.md +4 -4
  340. package/dist/tutorials/Troubleshooting.md +44 -44
  341. package/dist/tutorials/Uninstalling-and-Moving.md +5 -5
  342. package/dist/tutorials/Updating-and-Patch-Notes.md +18 -18
  343. package/dist/tutorials/Video-Editing.md +34 -34
  344. package/dist/tutorials/Where-Your-Files-Live.md +10 -10
  345. package/dist/tutorials/YouTube-Description.md +26 -26
  346. package/dist/tutorials/Your-First-Short.md +35 -35
  347. package/dist/tutorials/_Sidebar.md +15 -15
  348. package/dist/web/assets/index-BNftKQrA.css +1 -0
  349. package/dist/web/assets/index-Cpa58woP.js +459 -0
  350. package/dist/web/assets/pdf-BKh8f7R6.js +56 -0
  351. package/dist/web/assets/pdf.worker.min-BmVo14Nb.mjs +28 -0
  352. package/dist/web/assets/pdf.worker.min-D7i8o3wR.js +1 -0
  353. package/dist/web/index.html +2 -2
  354. package/package.json +2 -2
  355. package/dist/tutorials/Library-Overview.md +0 -76
  356. package/dist/tutorials/Play-Images.md +0 -165
  357. package/dist/tutorials/Play-Narration.md +0 -167
  358. package/dist/tutorials/Play-Outputs.md +0 -109
  359. package/dist/tutorials/Play-Overview.md +0 -227
  360. package/dist/tutorials/Play-Title-and-Article.md +0 -136
  361. package/dist/tutorials/Play-Video-and-Style.md +0 -143
  362. package/dist/web/assets/index-CkAI_2E7.css +0 -1
  363. package/dist/web/assets/index-DcD0cdfo.js +0 -334
  364. package/dist/web/assets/pdf-TODRwF31.js +0 -54
  365. package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +0 -29
  366. package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +0 -1
@@ -13,6 +13,17 @@ export { slotLabels, usageKinds, usageSlots } from "./model.js";
13
13
  function familyOf(slot) {
14
14
  return slot === "llm" ? "llm" : slot === "audio" ? "tts" : "image";
15
15
  }
16
+ // A Create draft's name as Create's header says it: its title once every keyword in it is
17
+ // filled, else "Untitled draft". Never a raw {{Topic}}.
18
+ function draftName(title, values) {
19
+ const filled = title
20
+ .replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (keyword, name) => {
21
+ const value = values[name]?.trim() ?? "";
22
+ return value === "" ? keyword : value;
23
+ })
24
+ .trim();
25
+ return filled === "" || filled.includes("{{") ? "Untitled draft" : filled;
26
+ }
16
27
  // The model choices a Play form (a draft or a template) makes.
17
28
  export function formChoices(form) {
18
29
  const edit = form.videoEdit;
@@ -155,7 +166,7 @@ export function choiceSites(db) {
155
166
  choices: formChoices(template.document.form),
156
167
  blocked: head === row.template_version
157
168
  ? null
158
- : `This schedule runs an older version of the template "${template.name}". In Library → Schedules, choose Edit, pick the template again so it runs the latest version, then save.`,
169
+ : `This schedule runs an older version of the template "${template.name}". In Calendar → Schedules, choose Edit, pick the template again so it runs the latest version, then save.`,
159
170
  });
160
171
  }
161
172
  for (const row of draftRows(db)) {
@@ -167,7 +178,7 @@ export function choiceSites(db) {
167
178
  sites.push({
168
179
  kind: "draft",
169
180
  id: row.id,
170
- name: row.title.trim() === "" ? "Untitled draft" : row.title,
181
+ name: draftName(row.title, document.form.values),
171
182
  choices: formChoices(document.form),
172
183
  blocked: null,
173
184
  });
@@ -2,6 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
2
2
  import { open, stat } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { errorText } from "../../kernel/errors.js";
5
+ import { editSettings } from "../../kernel/ports/settings-places.js";
5
6
  import { runFfmpeg } from "../video/ffmpeg.js";
6
7
  import { breathRate, scanBreaths, softenChunk } from "./breaths-detect.js";
7
8
  // Soften breaths, made in the join (`breaths-model.ts` says why there). Each narration piece is
@@ -39,7 +40,7 @@ export async function softenBreaths(run, files, directory) {
39
40
  catch (error) {
40
41
  if (run.signal.aborted)
41
42
  throw error;
42
- throw new Error(`Soften breaths could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften breaths in Edit project → Pauses and volume and use Save changes, then use Download diagnostics in Settings and report it.`, { cause: error });
43
+ throw new Error(`Soften breaths could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften breaths in ${editSettings.sound} and use Save changes, then use Download diagnostics in Settings and report it.`, { cause: error });
43
44
  }
44
45
  finally {
45
46
  rmSync(raw, { force: true });
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { errorText } from "../../kernel/errors.js";
4
+ import { editSettings } from "../../kernel/ports/settings-places.js";
4
5
  import { runFfmpeg } from "../video/ffmpeg.js";
5
6
  import { softenBreaths } from "./breaths.js";
6
7
  import { deEsser } from "./breaths-model.js";
@@ -37,7 +38,7 @@ export async function deEssPieces(run, files, directory) {
37
38
  catch (error) {
38
39
  if (run.signal.aborted)
39
40
  throw error;
40
- throw new Error(`Soften S sounds could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften S sounds in Edit project → Pauses and volume and use Save changes, then use Download diagnostics in Settings and report it.`, { cause: error });
41
+ throw new Error(`Soften S sounds could not process narration piece ${String(index + 1)} (${errorText(error)}), so the narration can't be joined together. Use Try again; if it fails again, turn off Soften S sounds in ${editSettings.sound} and use Save changes, then use Download diagnostics in Settings and report it.`, { cause: error });
41
42
  }
42
43
  written.push(output);
43
44
  }
@@ -9,9 +9,10 @@
9
9
  // A paragraph's minimum is separate and 0 (the sentence minimum only) unless set: the pause
10
10
  // between paragraphs is mostly the narration prompt's and the voice's to make.
11
11
  //
12
- // Browser-safe: Play and Edit project read these limits. A run without either has neither on
12
+ // Browser-safe: Create and Edit settings read these limits. A run without either has neither on
13
13
  // its config, which is what every project saved before them was, and the joins keep their
14
14
  // fingerprints then.
15
+ import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
15
16
  // ceiling: a quarter second reads brisk, 0.35 to 0.45 natural or documentary, 0.5 to 0.8 an
16
17
  // audiobook's relaxed pace, and above 0.8 sleep content; beyond 2 s it stops sounding like a
17
18
  // pause between sentences.
@@ -33,7 +34,7 @@ export function pauseProblem(value, name) {
33
34
  value <= pauseSecondsMax &&
34
35
  Math.abs(steps - Math.round(steps)) < 1e-6
35
36
  ? undefined
36
- : `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under Play → Video and style, or Edit project → Pauses and volume.`;
37
+ : `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under ${createRows.video}, or ${editSettings.sound}.`;
37
38
  }
38
39
  export function pauseFields(config) {
39
40
  const fields = [];
@@ -1,6 +1,7 @@
1
1
  import { remark } from "remark";
2
2
  import remarkGfm from "remark-gfm";
3
3
  import { internalError } from "../../kernel/errors.js";
4
+ import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
4
5
  import { remembered } from "../../kernel/remembered.js";
5
6
  export const currentPronunciationRules = 2;
6
7
  export function pronunciationRulesOf(config) {
@@ -154,7 +155,7 @@ function parseGlossary(markdown, language, rules = currentPronunciationRules) {
154
155
  export function skippedGlossaryNotice(skipped, unpronounced = 0) {
155
156
  if (skipped.length === 0 && unpronounced === 0)
156
157
  return null;
157
- const where = "in the Pronunciation Glossary at the end of the article in Edit project → Article";
158
+ const where = `in the Pronunciation Glossary at the end of the article in ${editSettings.article}`;
158
159
  const blank = unpronounced === 0
159
160
  ? ""
160
161
  : `${unpronounced === 1 ? "1 name has" : `${unpronounced} names have`} no pronunciation given and ${unpronounced === 1 ? "is" : "are"} read as written. To give ${unpronounced === 1 ? "it one" : "them one"}, fill in its IPA ${where}.`;
@@ -179,7 +180,7 @@ export function skippedSpeakerPronunciationsNotice(rows) {
179
180
  return null;
180
181
  const parts = rows.map((row) => `${row.speaker}: ${row.skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ")}`);
181
182
  return (`Speaker pronunciations: some entries are skipped and read as ordinary text (${parts.join(". ")}). ` +
182
- "The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (Play → Narration, or Edit project → Providers).");
183
+ `The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (${createRows.narration}, or ${editSettings.narration}).`);
183
184
  }
184
185
  // Remembered (`kernel/remembered.ts`): every plan rebuild matches the whole narration again.
185
186
  export const pronunciationMatches = remembered(1024, (source, entries, possessives) => `${possessives === true ? "p" : "-"}\u0000${source}\u0000${JSON.stringify(entries)}`, matchPronunciations);
@@ -4,7 +4,7 @@ import { stepFailedWhileWaiting } from "../admission/state-words.js";
4
4
  import { lastStarter } from "../agents/repo.js";
5
5
  import { projectHolds } from "../reviews/holds.js";
6
6
  import { isTemplateSample } from "../template-studio/repo.js";
7
- import { uploadedAndSettled } from "../uploads/repo.js";
7
+ import { settledProjects } from "../uploads/repo.js";
8
8
  import { settledStatus } from "../uploads/rules.js";
9
9
  import { atItsDay, failuresOf } from "./rules.js";
10
10
  // What a notification says about a project, read at the moment it goes out, by the rule the
@@ -14,7 +14,7 @@ export function noticeSubject(db, projectId) {
14
14
  if (project === undefined || isTemplateSample(db, projectId))
15
15
  return undefined;
16
16
  const stages = stagesOf(db, projectId);
17
- const settled = uploadedAndSettled(db, projectId);
17
+ const settled = settledProjects(db, projectId);
18
18
  const holds = projectHolds(db, { projectId, standings: new Map([[projectId, stages]]), settled });
19
19
  const status = settledStatus(derive(stages, project.paused), settled.has(projectId));
20
20
  const failures = failuresOf(stages);
@@ -12,7 +12,7 @@ import { projectDir } from "../storage/layout.js";
12
12
  // ceiling: one pass per layer of the step graph (text, narration, timing, picks, prompts,
13
13
  // images, renders); far more than any plan has.
14
14
  const rebindPasses = 40;
15
- export function copyProject(deps, sourceId, title) {
15
+ export function copyProject(deps, sourceId, title, { renamed = false } = {}) {
16
16
  const source = projectDir(deps.paths, sourceId);
17
17
  const tables = projectTables.map((table) => [table, projectRows(deps.db, table, sourceId)]);
18
18
  const ids = new Map([[sourceId, deps.ids.next()]]);
@@ -34,6 +34,9 @@ export function copyProject(deps, sourceId, title) {
34
34
  const projectId = ids.get(sourceId) ?? sourceId;
35
35
  const target = projectDir(deps.paths, projectId);
36
36
  const at = deps.clock.now().toISOString();
37
+ const named = (row) => renamed && typeof row.config === "string"
38
+ ? { ...row, config: renameConfig(row.config, title) }
39
+ : row;
37
40
  try {
38
41
  for (const file of listFiles(source)) {
39
42
  if (file === projectMarker)
@@ -52,7 +55,11 @@ export function copyProject(deps, sourceId, title) {
52
55
  column,
53
56
  typeof value === "string" ? remap(value) : value,
54
57
  ]));
55
- insert(deps.db, table, table === "projects" ? { ...copied, title, created_at: at, updated_at: at } : copied);
58
+ insert(deps.db, table, table === "projects"
59
+ ? { ...named(copied), title, created_at: at, updated_at: at }
60
+ : table === "project_revisions"
61
+ ? named(copied)
62
+ : copied);
56
63
  }
57
64
  rebind(deps, projectId);
58
65
  });
@@ -63,6 +70,16 @@ export function copyProject(deps, sourceId, title) {
63
70
  }
64
71
  return { projectId };
65
72
  }
73
+ // A stored config (JSON) under a new title, keeping what it was made about.
74
+ function renameConfig(config, title) {
75
+ const parsed = JSON.parse(config);
76
+ if (typeof parsed !== "object" || parsed === null || !("title" in parsed))
77
+ return config;
78
+ const before = parsed;
79
+ if (typeof before.title !== "string" || before.title === title)
80
+ return config;
81
+ return JSON.stringify({ ...before, title, subjectTitle: before.subjectTitle ?? before.title });
82
+ }
66
83
  // Re-stamps the copy's finished steps with the fingerprints its own ids give them.
67
84
  function rebind(deps, projectId) {
68
85
  const revisionId = currentRevisionId(deps.db, projectId);
@@ -1,14 +1,17 @@
1
- import { readinessIsUsable } from "../../kernel/ports/model.js";
2
1
  import { templateById } from "../project-templates/repo.js";
2
+ import { listVoices } from "../settings/repo.js";
3
3
  import { notTemplateSample } from "../template-studio/repo.js";
4
- import { starterPacks } from "./packs.js";
4
+ import { starterPacks, starterSet } from "./packs.js";
5
+ import { shortReadiness } from "./quick-short.js";
5
6
  import { sampleProjectIds } from "./sample.js";
6
7
  import { firstRunDismissed, readPackRecords } from "./state.js";
7
8
  // The first-run screen shows on a fresh install and never again once it is skipped or a real
8
9
  // project (anything but the samples and the template studio's hidden projects) exists. Reading it writes nothing: a real project that
9
10
  // exists before the screen was recorded as done is reported as `settle`, and the client
10
11
  // records it with POST /dismiss, so deleting every project later does not bring it back.
11
- export function firstRunView(db, statuses) {
12
+ export function firstRunView(db, statuses,
13
+ // How to install the computer's voice here, for the checklist's voice line.
14
+ install) {
12
15
  const samples = sampleProjectIds(db);
13
16
  const real = db
14
17
  .prepare(`SELECT 1 FROM projects WHERE id IS NOT ? AND id IS NOT ? AND id IS NOT ? AND ${notTemplateSample()} LIMIT 1`)
@@ -18,10 +21,15 @@ export function firstRunView(db, statuses) {
18
21
  return {
19
22
  show: !real && !dismissed,
20
23
  settle: real && !dismissed,
21
- voice: firstVoice(statuses),
24
+ // Every starter pack suggests the same voice, so the starter set's stands for all of them.
25
+ needs: shortReadiness({
26
+ statuses,
27
+ voices: listVoices(db),
28
+ suggested: starterSet.voice,
29
+ install,
30
+ }),
22
31
  sampleProjectId: samples.library,
23
32
  samples,
24
- clis: detectedClis(statuses),
25
33
  // A pack counts as added only with its template: the first short adds a pack's prompts and
26
34
  // voice but no template, and Add pack must still be able to add that template later (or
27
35
  // again, after it was deleted).
@@ -38,36 +46,3 @@ export function firstRunView(db, statuses) {
38
46
  }),
39
47
  };
40
48
  }
41
- // Who would narrate a first short: a keyed voice provider when one has a key, else the
42
- // computer's own voice when a speech program was found.
43
- export function firstVoice(statuses) {
44
- const keyed = statuses.find((row) => row.family === "tts" && row.readiness.kind === "keyed" && readinessIsUsable(row.readiness));
45
- const system = statuses.find((row) => row.readiness.kind === "local")?.readiness;
46
- const local = system?.kind === "local" ? system : undefined;
47
- return {
48
- keyed: keyed?.displayName ?? null,
49
- system: {
50
- available: local?.available === true,
51
- engine: local?.engine ?? null,
52
- issue: local?.issue ?? null,
53
- },
54
- };
55
- }
56
- // What this machine can make without a key: each agent CLI Slopify found, and whether it
57
- // answers (installed, a supported version, signed in as far as the probe can tell).
58
- export function detectedClis(statuses) {
59
- return ["claude-code", "codex", "gemini"].map((id) => {
60
- const status = statuses.find((row) => row.id === id);
61
- const readiness = status?.readiness;
62
- const cli = readiness?.kind === "cli" ? readiness : undefined;
63
- return {
64
- id,
65
- name: status?.displayName ?? id,
66
- installed: cli?.installed === true,
67
- ready: cli?.installed === true && cli.issue === undefined,
68
- version: cli?.version ?? null,
69
- issue: cli?.issue ?? null,
70
- draws: id === "codex",
71
- };
72
- });
73
- }
@@ -5,6 +5,7 @@ import { nameMax } from "../library/model.js";
5
5
  import { insertPrompt, promptById, promptByName } from "../library/repo.js";
6
6
  import { templateById, templateSummaries } from "../project-templates/repo.js";
7
7
  import { createTemplate } from "../project-templates/service.js";
8
+ import { readProviderDefaults } from "../settings/first-run.js";
8
9
  import { insertVoice, listVoices } from "../settings/repo.js";
9
10
  import { packById } from "./packs.js";
10
11
  import { readPackRecords, writePackRecord } from "./state.js";
@@ -46,7 +47,7 @@ export function installPack(deps, packId, options = { template: true }) {
46
47
  const created = createTemplate(deps, {
47
48
  id,
48
49
  name: freeTemplateName(deps, `${pack.name} starter`),
49
- document: packTemplate(pack, names, voice.voiceId),
50
+ document: packTemplate(pack, names, voice.voiceId, readProviderDefaults(deps.db)),
50
51
  });
51
52
  if (!created.ok)
52
53
  throw new Error(internalError(`the ${pack.name} pack's template was refused: ${created.reason}`));
@@ -10,6 +10,25 @@ export const sampleProjectsSchema = z.object({
10
10
  audiobook: z.string().nullable(),
11
11
  podcast: z.string().nullable(),
12
12
  });
13
+ // What a 60-second short needs that nothing on this computer does yet, with what fixes it. The
14
+ // first-run checklist and the refusal of "Make a 60-second short" say the same gaps.
15
+ export const shortGapSchema = z.object({
16
+ need: z.enum(["text", "voice", "images"]),
17
+ // What is missing and what fixes it, in a sentence or two.
18
+ message: z.string(),
19
+ // A key for that part, or (the voice) a voice saved for the voice service that has a key.
20
+ fix: z.enum(["key", "saved-voice"]),
21
+ // The voice only: how to install the computer's own free voice from a terminal, where that
22
+ // can be done.
23
+ install: z.object({ text: z.string(), commands: z.array(z.string()) }).optional(),
24
+ });
25
+ // Who does one part of a first short, by name ("Claude Code CLI", "OpenAI", "eSpeak NG"), and
26
+ // how: with the person's own sign-in to a tool on this computer, a saved key, or (the voice)
27
+ // the computer's own speech. Null while nothing can.
28
+ const shortPartSchema = z.object({
29
+ by: z.string().nullable(),
30
+ how: z.enum(["sign-in", "key", "computer"]).nullable(),
31
+ });
13
32
  export const firstRunViewSchema = z
14
33
  .object({
15
34
  show: z.boolean(),
@@ -17,31 +36,17 @@ export const firstRunViewSchema = z
17
36
  // with POST /dismiss, so deleting every project later does not bring the screen back.
18
37
  // Reading this view writes nothing.
19
38
  settle: z.boolean(),
20
- // Who narrates a first short: a voice provider with a saved key, else the computer's own.
21
- voice: z.object({
22
- // The keyed voice provider that would be used, by name ("OpenAI"), or null.
23
- keyed: z.string().nullable(),
24
- system: z.object({
25
- available: z.boolean(),
26
- // The speech program found ("eSpeak NG", "macOS voices").
27
- engine: z.string().nullable(),
28
- // Why none can be used, with the fix.
29
- issue: z.string().nullable(),
30
- }),
39
+ // The three things a short needs (the script, a voice and the images), each with who would
40
+ // do it, and the gaps, as "Make a 60-second short" would refuse them.
41
+ needs: z.object({
42
+ text: shortPartSchema,
43
+ voice: shortPartSchema,
44
+ images: shortPartSchema,
45
+ gaps: z.array(shortGapSchema),
31
46
  }),
32
47
  // The Library of Alexandria's project, as before the demos; `samples` has all three.
33
48
  sampleProjectId: z.string().nullable(),
34
49
  samples: sampleProjectsSchema,
35
- clis: z.array(z.object({
36
- id: z.enum(["claude-code", "codex", "gemini"]),
37
- name: z.string(),
38
- installed: z.boolean(),
39
- ready: z.boolean(),
40
- version: z.string().nullable(),
41
- issue: z.string().nullable(),
42
- // Codex draws the images too.
43
- draws: z.boolean(),
44
- })),
45
50
  packs: z.array(z.object({
46
51
  id: z.string(),
47
52
  name: z.string(),
@@ -0,0 +1,14 @@
1
+ // The model a service starts with when Slopify picks it for the person (a first short, the
2
+ // writer and voice Create fills in): a fast, capable default rather than the first listed,
3
+ // which can be the slowest or the smallest. One rule, so a service names the same model
4
+ // wherever it was picked for the person.
5
+ const preferred = {
6
+ "claude-code": /sonnet/i,
7
+ "openai-tts": /^gpt-4o-mini-tts$/,
8
+ };
9
+ export function isPreferredModel(provider, model) {
10
+ return preferred[provider]?.test(model) === true;
11
+ }
12
+ export function preferredModel(provider, models) {
13
+ return models.find((model) => isPreferredModel(provider, model.id)) ?? models[0];
14
+ }
@@ -2,18 +2,82 @@ import { readinessIsUsable } from "../../kernel/ports/model.js";
2
2
  import { titleMax } from "../admission/rules.js";
3
3
  import { defaultSentencePauseSeconds } from "../narration/pauses-model.js";
4
4
  import { systemVoiceProvider } from "../settings/model.js";
5
- const textClis = ["claude-code", "codex", "gemini"];
6
- const keyedImages = ["openai-image", "google-image", "fal", "replicate"];
5
+ import { preferredModel } from "./preferred-model.js";
6
+ // Who is asked first for each part: the CLIs before a key for the text, Codex before a key for
7
+ // the images.
8
+ const textOrder = ["claude-code", "codex", "gemini", "openrouter"];
9
+ const imageOrder = ["codex-image", "openai-image", "google-image", "fal", "replicate"];
7
10
  const keyedVoices = ["openai-tts", "elevenlabs", "cartesia", "inworld"];
8
- // The first model of these providers is the one a first short uses, unless one of these is
9
- // listed: a fast, capable default rather than the slowest or the smallest.
10
- const preferred = {
11
- "claude-code": /sonnet/i,
12
- "openai-tts": /^gpt-4o-mini-tts$/,
13
- };
14
11
  function ready(status) {
15
12
  return status !== undefined && readinessIsUsable(status.readiness);
16
13
  }
14
+ const statusOf = (statuses) => (id) => statuses.find((row) => row.id === id);
15
+ // The keyed voice a first short narrates with: a saved voice of a voice service with a key,
16
+ // else the voice the pack suggests when its service has a key.
17
+ function keyedVoice(statuses, voices, suggested) {
18
+ const status = statusOf(statuses);
19
+ const saved = voices.find((voice) => voice.provider !== systemVoiceProvider && ready(status(voice.provider)));
20
+ if (saved !== undefined)
21
+ return { provider: saved.provider, voiceId: saved.voiceId };
22
+ return ready(status(suggested.provider))
23
+ ? { provider: suggested.provider, voiceId: suggested.voiceId }
24
+ : undefined;
25
+ }
26
+ // One gap per part nothing can do, in plain words with what fixes it. The voice names a voice
27
+ // service that has a key but no saved voice, since a key alone is not the fix then.
28
+ function gapOf(need, statuses, install) {
29
+ if (need === "text")
30
+ return {
31
+ need,
32
+ fix: "key",
33
+ message: "Nothing can write the article yet. Sign in to Claude Code, Codex or Antigravity on this computer, or add an OpenRouter key.",
34
+ };
35
+ if (need === "images")
36
+ return {
37
+ need,
38
+ fix: "key",
39
+ message: "Nothing can make the images yet. Add an image key (OpenAI, Google, fal.ai or Replicate), or sign in to Codex on this computer.",
40
+ };
41
+ const status = statusOf(statuses);
42
+ const unsaved = keyedVoices.map(status).find((row) => ready(row));
43
+ if (unsaved !== undefined)
44
+ return {
45
+ need,
46
+ fix: "saved-voice",
47
+ message: `${unsaved.displayName} has a key but no voice saved yet. Save one in Settings → Voices, or add an OpenAI key, which works at once.`,
48
+ };
49
+ return {
50
+ need,
51
+ fix: "key",
52
+ message: "Nothing can read the article aloud yet. Add a voice key: an OpenAI key works at once with the voices Slopify sets up.",
53
+ ...(install === undefined ? {} : { install: { ...install, commands: [...install.commands] } }),
54
+ };
55
+ }
56
+ // What a first short would use on this computer and what it lacks, without asking any provider
57
+ // for its models: the first-run checklist, which must agree with the refusal below.
58
+ export function shortReadiness(input) {
59
+ const status = statusOf(input.statuses);
60
+ const part = (row) => row === undefined
61
+ ? { by: null, how: null }
62
+ : { by: row.displayName, how: row.readiness.kind === "cli" ? "sign-in" : "key" };
63
+ const text = part(textOrder.map(status).find((row) => ready(row)));
64
+ const images = part(imageOrder.map(status).find((row) => ready(row)));
65
+ const keyed = keyedVoice(input.statuses, input.voices, input.suggested);
66
+ const system = status(systemVoiceProvider)?.readiness;
67
+ const voice = keyed !== undefined
68
+ ? part(status(keyed.provider))
69
+ : system?.kind === "local" && system.available
70
+ ? { by: system.engine ?? "System voice", how: "computer" }
71
+ : { by: null, how: null };
72
+ const gaps = [
73
+ ["text", text],
74
+ ["images", images],
75
+ ["voice", voice],
76
+ ]
77
+ .filter(([, done]) => done.by === null)
78
+ .map(([need]) => gapOf(need, input.statuses, input.install));
79
+ return { text, voice, images, gaps };
80
+ }
17
81
  async function firstModel(modelsFor, provider, family) {
18
82
  let models;
19
83
  try {
@@ -22,58 +86,37 @@ async function firstModel(modelsFor, provider, family) {
22
86
  catch {
23
87
  return undefined;
24
88
  }
25
- const liked = preferred[provider];
26
- return ((liked === undefined ? undefined : models.find((model) => liked.test(model.id)))?.id ??
27
- models[0]?.id);
89
+ return preferredModel(provider, models)?.id;
28
90
  }
29
- export async function planShortProviders(input) {
30
- const status = (id) => input.statuses.find((row) => row.id === id);
31
- const gaps = [];
32
- let llm;
33
- for (const id of [...textClis, "openrouter"]) {
91
+ // The first ready provider in `order` that lists a model.
92
+ async function firstReady(input, order, family) {
93
+ const status = statusOf(input.statuses);
94
+ for (const id of order) {
34
95
  if (!ready(status(id)))
35
96
  continue;
36
- const model = await firstModel(input.modelsFor, id, "llm");
37
- if (model !== undefined) {
38
- llm = { provider: id, model };
39
- break;
40
- }
41
- }
42
- if (llm === undefined)
43
- gaps.push({
44
- need: "text",
45
- message: "No text model is ready. Install Claude Code, Codex or Antigravity CLI and sign in to it (Settings → Providers shows what Slopify found), or add an OpenRouter key in Settings → Providers.",
46
- });
47
- let images;
48
- for (const id of ["codex-image", ...keyedImages]) {
49
- if (!ready(status(id)))
50
- continue;
51
- const model = await firstModel(input.modelsFor, id, "image");
52
- if (model !== undefined) {
53
- images = { provider: id, model };
54
- break;
55
- }
97
+ const model = await firstModel(input.modelsFor, id, family);
98
+ if (model !== undefined)
99
+ return { provider: id, model };
56
100
  }
57
- if (images === undefined)
58
- gaps.push({
59
- need: "images",
60
- message: "No image model is ready. Install Codex CLI and sign in to it, or add an OpenAI, Google, fal.ai or Replicate key in Settings → Providers.",
61
- });
101
+ return undefined;
102
+ }
103
+ export async function planShortProviders(input) {
104
+ const status = statusOf(input.statuses);
105
+ const llm = await firstReady(input, textOrder, "llm");
106
+ const images = await firstReady(input, imageOrder, "image");
62
107
  // A keyed voice first (a saved one, else the pack's suggestion), then the computer's own: a
63
108
  // saved system voice, else the one found. Keys are the better voice, never a requirement.
64
109
  let audio;
65
- const keyed = (provider) => provider !== systemVoiceProvider;
66
- const saved = input.voices.find((voice) => keyed(voice.provider) && ready(status(voice.provider)));
67
- const provider = saved?.provider ??
68
- (ready(status(input.suggested.provider)) ? input.suggested.provider : undefined);
69
- if (provider !== undefined) {
70
- const model = await firstModel(input.modelsFor, provider, "tts");
110
+ const keyed = keyedVoice(input.statuses, input.voices, input.suggested);
111
+ if (keyed !== undefined) {
112
+ const model = await firstModel(input.modelsFor, keyed.provider, "tts");
71
113
  if (model !== undefined)
72
- audio = { provider, model, voice: saved?.voiceId ?? input.suggested.voiceId };
114
+ audio = { provider: keyed.provider, model, voice: keyed.voiceId };
73
115
  }
74
116
  let systemVoice = false;
75
- const system = status(systemVoiceProvider);
76
- if (audio === undefined && ready(system) && input.systemVoice !== undefined) {
117
+ if (audio === undefined &&
118
+ ready(status(systemVoiceProvider)) &&
119
+ input.systemVoice !== undefined) {
77
120
  const savedSystem = input.voices.find((voice) => voice.provider === systemVoiceProvider);
78
121
  audio = {
79
122
  provider: systemVoiceProvider,
@@ -82,20 +125,16 @@ export async function planShortProviders(input) {
82
125
  };
83
126
  systemVoice = true;
84
127
  }
85
- if (audio === undefined) {
86
- const systemIssue = system?.readiness.kind === "local" && system.readiness.issue !== undefined
87
- ? ` ${system.readiness.issue}`
88
- : "";
89
- gaps.push({
90
- need: "voice",
91
- message: keyedVoices.some((id) => ready(status(id)))
92
- ? "Your voice provider has no saved voice yet. Add one in Settings → Voices, then try again."
93
- : `No voice is ready to narrate the short: no voice key is saved and this computer's built-in voice can't be used.${systemIssue || " Install espeak-ng, or add an OpenAI, ElevenLabs, Cartesia or Inworld key in Settings → Providers (with ElevenLabs, Cartesia or Inworld, also save a voice in Settings → Voices)."}`,
94
- });
95
- }
96
- return llm === undefined || images === undefined || audio === undefined
97
- ? { ok: false, gaps }
98
- : { ok: true, providers: { llm, audio, images }, systemVoice };
128
+ if (llm === undefined || images === undefined || audio === undefined)
129
+ return {
130
+ ok: false,
131
+ gaps: [
132
+ ...(llm === undefined ? [gapOf("text", input.statuses, input.install)] : []),
133
+ ...(images === undefined ? [gapOf("images", input.statuses, input.install)] : []),
134
+ ...(audio === undefined ? [gapOf("voice", input.statuses, input.install)] : []),
135
+ ],
136
+ };
137
+ return { ok: true, providers: { llm, audio, images }, systemVoice };
99
138
  }
100
139
  // The short-mode draft for a topic, using the pack's (or starter set's) script and scene
101
140
  // prompts under the names they have in the library.
@@ -127,5 +127,8 @@ export function copySample(deps, projectId) {
127
127
  status: 404,
128
128
  detail: "That sample project is no longer here. Bring it back with Settings → Backup & storage → Restore samples, then press Make my own copy.",
129
129
  };
130
- return { ok: true, ...copyProject(deps, projectId, `${title} (my copy)`) };
130
+ return {
131
+ ok: true,
132
+ ...copyProject(deps, projectId, `${title} (my copy)`, { renamed: true }),
133
+ };
131
134
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readJsonSetting, readSetting, writeSetting } from "../settings/repo.js";
3
+ import { sampleIds } from "./model.js";
3
4
  // What the first five minutes remember, in the settings table: whether the first-run screen
4
5
  // was dismissed, which project is the bundled sample, and which library rows each installed
5
6
  // starter pack became. None of these keys is in a portable backup's settings, so another
@@ -41,6 +42,12 @@ const sampleKeyOf = (id) => (id === "library" ? sampleKey : `${sampleKey}.${id}`
41
42
  export function readSampleRecord(db, id = "library") {
42
43
  return readJson(db, sampleKeyOf(id), sampleRecord);
43
44
  }
45
+ // The bundled samples' projects, by id, for a slice that must leave them as they are (Disk
46
+ // space's clean-ups). A deleted sample's record stays until Restore samples, so an id may name
47
+ // no project any more.
48
+ export function bundledSampleProjects(db) {
49
+ return new Set(sampleIds.flatMap((id) => readSampleRecord(db, id)?.projectId ?? []));
50
+ }
44
51
  export function writeSampleRecord(db, record, id = "library") {
45
52
  writeSetting(db, sampleKeyOf(id), JSON.stringify(record));
46
53
  }
@@ -1,11 +1,12 @@
1
1
  import { defaultEdgeSilenceSeconds } from "../admission/rules.js";
2
2
  import { playDraftDocumentSchema } from "../play-drafts/schema.js";
3
3
  // A pack's Play template: its prompts ticked, its voice picked, its style set, and the text and
4
- // image providers left for the user to choose, since which of them is installed or keyed
5
- // differs per machine.
6
- export function packTemplate(pack, names, voiceId) {
4
+ // image providers this machine's first launch found (Claude Code, Codex), since which of them
5
+ // is installed or keyed differs per machine. One it did not find is left for the user to pick.
6
+ export function packTemplate(pack, names, voiceId, providers = {}) {
7
7
  const { style } = pack;
8
8
  const name = (key) => names[key] ?? "";
9
+ const chosen = (choice) => choice === undefined ? { provider: "", model: "" } : { ...choice };
9
10
  return playDraftDocumentSchema.parse({
10
11
  schemaVersion: 1,
11
12
  form: {
@@ -21,7 +22,7 @@ export function packTemplate(pack, names, voiceId) {
21
22
  video: "generate",
22
23
  document: "off",
23
24
  },
24
- llm: { provider: "", model: "" },
25
+ llm: chosen(providers.llm),
25
26
  audio: {
26
27
  provider: pack.voice.provider,
27
28
  model: pack.voice.model,
@@ -30,7 +31,7 @@ export function packTemplate(pack, names, voiceId) {
30
31
  shareGlossary: true,
31
32
  describeFigures: true,
32
33
  },
33
- images: { provider: "", model: "" },
34
+ images: chosen(providers.images),
34
35
  articlePrompt: name("article"),
35
36
  narrationPrompt: "",
36
37
  youtubeDescription: name("description") !== "",