@gentbajko/slopify 4.0.0 → 4.2.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 (375) 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 +5 -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/pronunciation-check.js +133 -0
  59. package/dist/edge/http/pronunciations.js +26 -0
  60. package/dist/edge/http/providers.js +9 -10
  61. package/dist/edge/http/refusal.js +1 -1
  62. package/dist/edge/http/remote-access.js +4 -4
  63. package/dist/edge/http/remote-guard.js +2 -2
  64. package/dist/edge/http/reviews.js +1 -1
  65. package/dist/edge/http/revision-files.js +1 -1
  66. package/dist/edge/http/revisions.js +4 -4
  67. package/dist/edge/http/schedules.js +13 -11
  68. package/dist/edge/http/settings.js +6 -2
  69. package/dist/edge/http/social.js +5 -4
  70. package/dist/edge/http/staging.js +1 -1
  71. package/dist/edge/http/storage.js +12 -4
  72. package/dist/edge/http/studio.js +71 -19
  73. package/dist/edge/http/style-preview.js +2 -2
  74. package/dist/edge/http/template-studio.js +12 -9
  75. package/dist/edge/http/trash.js +1 -1
  76. package/dist/edge/http/youtube-edits.js +1 -1
  77. package/dist/edge/mcp/excluded-routes.js +4 -3
  78. package/dist/edge/mcp/instructions.js +3 -3
  79. package/dist/edge/mcp/library-tools.js +8 -5
  80. package/dist/edge/mcp/remake-tools.js +7 -4
  81. package/dist/edge/mcp/server.js +1 -1
  82. package/dist/edge/mcp/tools-channels.js +9 -9
  83. package/dist/edge/mcp/tools-designs.js +9 -9
  84. package/dist/edge/mcp/tools-library.js +31 -7
  85. package/dist/edge/mcp/tools-planning.js +18 -18
  86. package/dist/edge/mcp/tools-projects.js +10 -10
  87. package/dist/edge/mcp/tools-publishing.js +9 -9
  88. package/dist/edge/mcp/tools-runs.js +9 -9
  89. package/dist/edge/mcp/tools-schedules.js +6 -6
  90. package/dist/edge/mcp/tools-setup.js +20 -14
  91. package/dist/edge/mcp/tools-social.js +5 -4
  92. package/dist/edge/mcp/tools-storage.js +3 -3
  93. package/dist/edge/mcp/tools-template-studio.js +18 -18
  94. package/dist/edge/remote-access/model.js +1 -1
  95. package/dist/extension/slopify-studio-chrome.zip +0 -0
  96. package/dist/extension/slopify-studio-firefox.zip +0 -0
  97. package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
  98. package/dist/kernel/pipeline.js +12 -0
  99. package/dist/kernel/ports/settings-places.js +31 -0
  100. package/dist/kernel/ports/system-speech.js +31 -3
  101. package/dist/kernel/runner/attempt.js +11 -3
  102. package/dist/kernel/runner/providers.js +3 -2
  103. package/dist/kernel/runner/standalone.js +2 -2
  104. package/dist/main.js +5 -3
  105. package/dist/model-catalog.js +20 -0
  106. package/dist/patch-notes/4.0.0.md +13 -13
  107. package/dist/patch-notes/4.1.0.md +42 -0
  108. package/dist/patch-notes/4.2.0.md +28 -0
  109. package/dist/patch-notes/index.json +12 -0
  110. package/dist/slices/admission/listing-facts.js +95 -0
  111. package/dist/slices/admission/repo.js +9 -6
  112. package/dist/slices/admission/rules.js +7 -7
  113. package/dist/slices/admission/schema.js +2 -0
  114. package/dist/slices/admission/start.js +7 -0
  115. package/dist/slices/admission/state-words.js +34 -5
  116. package/dist/slices/assistant/cost.js +1 -1
  117. package/dist/slices/assistant/gate.js +1 -0
  118. package/dist/slices/assistant/model.js +10 -3
  119. package/dist/slices/assistant/turn.js +5 -3
  120. package/dist/slices/autopilot/warnings.js +1 -1
  121. package/dist/slices/backups/service.js +5 -5
  122. package/dist/slices/channels/cast-images.js +1 -1
  123. package/dist/slices/channels/schema.js +1 -1
  124. package/dist/slices/channels/videos.js +1 -1
  125. package/dist/slices/control/providers.js +2 -2
  126. package/dist/slices/designs/budget.js +1 -1
  127. package/dist/slices/designs/calls.js +2 -2
  128. package/dist/slices/designs/check-context.js +4 -4
  129. package/dist/slices/designs/fields.js +4 -4
  130. package/dist/slices/designs/gate.js +7 -5
  131. package/dist/slices/designs/render-input.js +1 -1
  132. package/dist/slices/designs/requests.js +1 -1
  133. package/dist/slices/designs/runner.js +1 -1
  134. package/dist/slices/designs/sample.js +2 -1
  135. package/dist/slices/designs/service.js +1 -1
  136. package/dist/slices/designs/template-checks.js +2 -2
  137. package/dist/slices/designs/template-make.js +3 -3
  138. package/dist/slices/designs/templates.js +3 -3
  139. package/dist/slices/document/fonts.js +1 -1
  140. package/dist/slices/document/model.js +7 -4
  141. package/dist/slices/document/render.js +2 -1
  142. package/dist/slices/document/theme-schema.js +1 -1
  143. package/dist/slices/document/theme.js +49 -3
  144. package/dist/slices/episodes/summarize.js +2 -2
  145. package/dist/slices/estimate/index.js +28 -22
  146. package/dist/slices/estimate/requests.js +4 -4
  147. package/dist/slices/fixes/rules.js +3 -1
  148. package/dist/slices/fonts/catalog.js +2 -1
  149. package/dist/slices/fonts/coverage.js +2 -1
  150. package/dist/slices/images/appearance.js +2 -1
  151. package/dist/slices/images/scenes.js +2 -1
  152. package/dist/slices/keywords/digest.js +2 -1
  153. package/dist/slices/keywords/rules.js +2 -2
  154. package/dist/slices/keywords/specs.js +5 -5
  155. package/dist/slices/keywords/values.js +1 -1
  156. package/dist/slices/library/transfer.js +3 -3
  157. package/dist/slices/loudness/loudnorm.js +2 -1
  158. package/dist/slices/loudness/model.js +3 -2
  159. package/dist/slices/loudness/sentence-level.js +2 -1
  160. package/dist/slices/model-checks/rules.js +13 -0
  161. package/dist/slices/model-upkeep/model.js +1 -1
  162. package/dist/slices/model-upkeep/switch.js +3 -3
  163. package/dist/slices/model-upkeep/usage.js +13 -2
  164. package/dist/slices/narration/breaths.js +2 -1
  165. package/dist/slices/narration/clean-pieces.js +2 -1
  166. package/dist/slices/narration/pauses-model.js +3 -2
  167. package/dist/slices/narration/pronunciation-check.js +226 -0
  168. package/dist/slices/narration/pronunciation.js +7 -2
  169. package/dist/slices/narration/pronunciations-library.js +94 -0
  170. package/dist/slices/notifications/subject.js +2 -2
  171. package/dist/slices/onboarding/copy.js +19 -2
  172. package/dist/slices/onboarding/first-run.js +13 -38
  173. package/dist/slices/onboarding/install.js +2 -1
  174. package/dist/slices/onboarding/model.js +26 -21
  175. package/dist/slices/onboarding/preferred-model.js +14 -0
  176. package/dist/slices/onboarding/quick-short.js +103 -64
  177. package/dist/slices/onboarding/sample.js +4 -1
  178. package/dist/slices/onboarding/state.js +7 -0
  179. package/dist/slices/onboarding/template.js +6 -5
  180. package/dist/slices/outputs/add.js +3 -2
  181. package/dist/slices/outputs/service.js +2 -2
  182. package/dist/slices/patch-notes/library.js +1 -1
  183. package/dist/slices/play-drafts/readiness.js +3 -3
  184. package/dist/slices/play-drafts/review-inputs.js +1 -1
  185. package/dist/slices/play-drafts/service.js +17 -7
  186. package/dist/slices/project-templates/next-chapter.js +2 -1
  187. package/dist/slices/project-templates/provider-defaults.js +22 -0
  188. package/dist/slices/project-templates/setup-sections.js +26 -0
  189. package/dist/slices/project-templates/setup.js +6 -1
  190. package/dist/slices/rebuild/preview-details.js +5 -5
  191. package/dist/slices/rebuild/recipe-preparation.js +1 -1
  192. package/dist/slices/rebuild/recipe-save.js +2 -2
  193. package/dist/slices/rebuild/recipe-text.js +16 -4
  194. package/dist/slices/rebuild/recipe-validation.js +5 -4
  195. package/dist/slices/rebuild/recipe-voices.js +3 -2
  196. package/dist/slices/rebuild/recipe-work.js +4 -4
  197. package/dist/slices/rebuild/recovery.js +6 -6
  198. package/dist/slices/rebuild/remake-outdated.js +5 -5
  199. package/dist/slices/rebuild/review-redo.js +1 -1
  200. package/dist/slices/rebuild/runtime-animate.js +2 -2
  201. package/dist/slices/rebuild/runtime-article.js +4 -3
  202. package/dist/slices/rebuild/runtime-document.js +5 -4
  203. package/dist/slices/rebuild/runtime-export-bed.js +2 -1
  204. package/dist/slices/rebuild/runtime-export-edit.js +2 -1
  205. package/dist/slices/rebuild/runtime-export-inputs.js +5 -4
  206. package/dist/slices/rebuild/runtime-export-short.js +1 -1
  207. package/dist/slices/rebuild/runtime-export.js +4 -3
  208. package/dist/slices/rebuild/runtime-image.js +4 -3
  209. package/dist/slices/rebuild/runtime-local.js +3 -2
  210. package/dist/slices/rebuild/runtime-narration-text.js +3 -2
  211. package/dist/slices/rebuild/runtime-provider.js +7 -6
  212. package/dist/slices/rebuild/runtime-review.js +6 -5
  213. package/dist/slices/rebuild/runtime-shorts.js +6 -5
  214. package/dist/slices/rebuild/runtime-store.js +17 -2
  215. package/dist/slices/rebuild/runtime-subtitles.js +13 -12
  216. package/dist/slices/rebuild/runtime-voices.js +2 -1
  217. package/dist/slices/rebuild/runtime-youtube.js +3 -2
  218. package/dist/slices/rebuild/service-readiness.js +11 -3
  219. package/dist/slices/research/synthesis.js +3 -2
  220. package/dist/slices/reviews/external-model.js +1 -1
  221. package/dist/slices/reviews/holds.js +5 -4
  222. package/dist/slices/reviews/model.js +8 -5
  223. package/dist/slices/reviews/rules.js +3 -3
  224. package/dist/slices/reviews/verdict.js +2 -1
  225. package/dist/slices/revisions/image-plan.js +1 -1
  226. package/dist/slices/revisions/mutation-assets.js +2 -1
  227. package/dist/slices/revisions/mutations.js +3 -2
  228. package/dist/slices/schedules/agenda.js +7 -5
  229. package/dist/slices/schedules/design-check.js +4 -4
  230. package/dist/slices/schedules/design-run.js +3 -3
  231. package/dist/slices/schedules/repo.js +6 -5
  232. package/dist/slices/schedules/schema.js +3 -0
  233. package/dist/slices/schedules/topics.js +5 -5
  234. package/dist/slices/settings/first-run.js +6 -32
  235. package/dist/slices/settings/health.js +30 -15
  236. package/dist/slices/settings/key-test.js +1 -1
  237. package/dist/slices/settings/model.js +4 -2
  238. package/dist/slices/settings/playback.js +2 -2
  239. package/dist/slices/shorts/keep.js +2 -1
  240. package/dist/slices/shorts/pick.js +4 -3
  241. package/dist/slices/shorts/prompts.js +2 -1
  242. package/dist/slices/shorts/render.js +3 -2
  243. package/dist/slices/social/media.js +5 -4
  244. package/dist/slices/social/pack.js +1 -1
  245. package/dist/slices/social/post-prompt.js +2 -1
  246. package/dist/slices/social/reshape.js +2 -2
  247. package/dist/slices/social/serve.js +1 -1
  248. package/dist/slices/social/service.js +3 -2
  249. package/dist/slices/social/state.js +1 -1
  250. package/dist/slices/storage/backup-import.js +2 -2
  251. package/dist/slices/storage/portable.js +1 -1
  252. package/dist/slices/storage/trim.js +23 -13
  253. package/dist/slices/studio/calendar.js +3 -1
  254. package/dist/slices/studio/disclosure.js +4 -4
  255. package/dist/slices/studio/model.js +1 -1
  256. package/dist/slices/studio/pack.js +9 -8
  257. package/dist/slices/studio/plan.js +1 -1
  258. package/dist/slices/studio/settings.js +6 -6
  259. package/dist/slices/studio/videos.js +23 -4
  260. package/dist/slices/style-preview/render.js +3 -3
  261. package/dist/slices/style-preview/schema.js +1 -1
  262. package/dist/slices/subtitles/captions.js +1 -1
  263. package/dist/slices/template-studio/anatomy-view.js +46 -3
  264. package/dist/slices/template-studio/bakeoff.js +3 -3
  265. package/dist/slices/template-studio/budget.js +6 -3
  266. package/dist/slices/template-studio/cards.js +78 -0
  267. package/dist/slices/template-studio/checks.js +99 -50
  268. package/dist/slices/template-studio/for-all.js +5 -5
  269. package/dist/slices/template-studio/free-video.js +1 -1
  270. package/dist/slices/template-studio/free.js +5 -5
  271. package/dist/slices/template-studio/model.js +19 -15
  272. package/dist/slices/template-studio/pack-import.js +14 -14
  273. package/dist/slices/template-studio/pack.js +9 -7
  274. package/dist/slices/template-studio/paid.js +12 -5
  275. package/dist/slices/template-studio/quote.js +18 -11
  276. package/dist/slices/template-studio/ready.js +7 -7
  277. package/dist/slices/template-studio/runner.js +1 -1
  278. package/dist/slices/template-studio/save.js +12 -12
  279. package/dist/slices/template-studio/say-it-as.js +3 -3
  280. package/dist/slices/template-studio/schedule-gate.js +1 -1
  281. package/dist/slices/template-studio/schedule-price.js +3 -1
  282. package/dist/slices/template-studio/schedule-sample.js +5 -1
  283. package/dist/slices/template-studio/schema.js +25 -0
  284. package/dist/slices/template-studio/service.js +3 -1
  285. package/dist/slices/template-studio/status.js +26 -9
  286. package/dist/slices/template-studio/topic.js +3 -3
  287. package/dist/slices/thumbnail/by-llm.js +1 -1
  288. package/dist/slices/uploads/repo.js +37 -1
  289. package/dist/slices/uploads/rules.js +3 -2
  290. package/dist/slices/video/edit-list.js +2 -2
  291. package/dist/slices/video/edit-settings.js +1 -1
  292. package/dist/slices/video/encoding.js +14 -5
  293. package/dist/slices/video/ffmpeg.js +2 -2
  294. package/dist/slices/video/plan.js +2 -1
  295. package/dist/slices/video/slideshow.js +1 -1
  296. package/dist/slices/voices/attribution.js +3 -2
  297. package/dist/slices/voices/model.js +9 -8
  298. package/dist/slices/voices/script.js +2 -1
  299. package/dist/slices/youtube/answer.js +2 -1
  300. package/dist/slices/youtube/edits-repo.js +1 -1
  301. package/dist/tutorials/AI-CLIs.md +25 -25
  302. package/dist/tutorials/Agents-MCP.md +32 -32
  303. package/dist/tutorials/Assistant.md +5 -5
  304. package/dist/tutorials/Backups.md +7 -7
  305. package/dist/tutorials/Calendar.md +17 -25
  306. package/dist/tutorials/Cast-Library.md +19 -19
  307. package/dist/tutorials/Channels.md +40 -35
  308. package/dist/tutorials/Costs-and-Run-Cost.md +38 -40
  309. package/dist/tutorials/Create-Article.md +114 -0
  310. package/dist/tutorials/Create-Extras.md +112 -0
  311. package/dist/tutorials/Create-Images.md +165 -0
  312. package/dist/tutorials/Create-Narration.md +178 -0
  313. package/dist/tutorials/Create-Overview.md +244 -0
  314. package/dist/tutorials/Create-Video-and-Style.md +144 -0
  315. package/dist/tutorials/Docker.md +3 -3
  316. package/dist/tutorials/Document-Themes.md +32 -32
  317. package/dist/tutorials/Editing-a-Project.md +95 -94
  318. package/dist/tutorials/FAQ.md +18 -18
  319. package/dist/tutorials/First-Launch-and-Welcome.md +50 -64
  320. package/dist/tutorials/Home-and-Projects.md +101 -76
  321. package/dist/tutorials/Home.md +33 -33
  322. package/dist/tutorials/Install.md +13 -13
  323. package/dist/tutorials/Intros-and-Outros.md +33 -34
  324. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +62 -58
  325. package/dist/tutorials/Keywords-Slopify-Fills.md +21 -21
  326. package/dist/tutorials/Models.md +27 -27
  327. package/dist/tutorials/Multiple-Voices.md +41 -40
  328. package/dist/tutorials/Narration-Aliases-and-Glossary.md +101 -19
  329. package/dist/tutorials/Notifications.md +21 -17
  330. package/dist/tutorials/Other-Languages.md +15 -15
  331. package/dist/tutorials/PDF-Documents.md +38 -29
  332. package/dist/tutorials/Posting-to-Social-Sites.md +12 -12
  333. package/dist/tutorials/Project-Page.md +118 -120
  334. package/dist/tutorials/Prompts-and-Templates.md +84 -0
  335. package/dist/tutorials/Prompts.md +45 -45
  336. package/dist/tutorials/Providers-and-Keys.md +105 -98
  337. package/dist/tutorials/Publishing-to-YouTube.md +31 -31
  338. package/dist/tutorials/Recovery-and-Retries.md +31 -31
  339. package/dist/tutorials/Remote-Access.md +2 -2
  340. package/dist/tutorials/Reviews-and-Checkpoints.md +90 -93
  341. package/dist/tutorials/Schedules.md +27 -28
  342. package/dist/tutorials/Settings-Reference.md +183 -204
  343. package/dist/tutorials/Shorts.md +34 -33
  344. package/dist/tutorials/Start-at-Login.md +3 -3
  345. package/dist/tutorials/Studio-Extension.md +22 -24
  346. package/dist/tutorials/Template-Studio.md +45 -42
  347. package/dist/tutorials/Templates.md +50 -79
  348. package/dist/tutorials/Trash.md +4 -4
  349. package/dist/tutorials/Troubleshooting.md +44 -44
  350. package/dist/tutorials/Uninstalling-and-Moving.md +5 -5
  351. package/dist/tutorials/Updating-and-Patch-Notes.md +18 -18
  352. package/dist/tutorials/Video-Editing.md +34 -34
  353. package/dist/tutorials/Where-Your-Files-Live.md +10 -10
  354. package/dist/tutorials/YouTube-Description.md +26 -26
  355. package/dist/tutorials/Your-First-Short.md +35 -35
  356. package/dist/tutorials/_Sidebar.md +15 -15
  357. package/dist/web/assets/index-BNftKQrA.css +1 -0
  358. package/dist/web/assets/index-CQASqwjT.js +460 -0
  359. package/dist/web/assets/pdf-BKh8f7R6.js +56 -0
  360. package/dist/web/assets/pdf.worker.min-BmVo14Nb.mjs +28 -0
  361. package/dist/web/assets/pdf.worker.min-D7i8o3wR.js +1 -0
  362. package/dist/web/index.html +2 -2
  363. package/package.json +2 -2
  364. package/dist/tutorials/Library-Overview.md +0 -76
  365. package/dist/tutorials/Play-Images.md +0 -165
  366. package/dist/tutorials/Play-Narration.md +0 -167
  367. package/dist/tutorials/Play-Outputs.md +0 -109
  368. package/dist/tutorials/Play-Overview.md +0 -227
  369. package/dist/tutorials/Play-Title-and-Article.md +0 -136
  370. package/dist/tutorials/Play-Video-and-Style.md +0 -143
  371. package/dist/web/assets/index-CkAI_2E7.css +0 -1
  372. package/dist/web/assets/index-DcD0cdfo.js +0 -334
  373. package/dist/web/assets/pdf-TODRwF31.js +0 -54
  374. package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +0 -29
  375. package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +0 -1
@@ -22,16 +22,16 @@ export function designCheckContext(deps) {
22
22
  statuses = await deps.providers();
23
23
  }
24
24
  catch (error) {
25
- return `Slopify couldn't check the image provider: ${errorText(error)} Open Settings → Providers, then press Mark ready again.`;
25
+ return `Slopify couldn't check the image service: ${errorText(error)} Open Settings → Keys & services, then press Approve for schedules again.`;
26
26
  }
27
27
  const status = statuses.find((one) => one.id === provider);
28
28
  if (status === undefined)
29
- return `The image provider “${provider}” isn't one this Slopify knows. Choose another image model under Picture.`;
29
+ return `The image service “${provider}” isn't one this Slopify knows. Choose another image model under Picture.`;
30
30
  const readiness = status.readiness;
31
31
  if (readiness.kind === "keyed" && !readiness.hasKey)
32
- return `${status.displayName} has no API key, so the picture can't be made. Add the key in Settings → Providers, then press Mark ready again.`;
32
+ return `${status.displayName} has no API key, so the picture can't be made. Add the key in Settings → Keys & services, then press Approve for schedules again.`;
33
33
  if (readiness.kind === "cli" && (!readiness.installed || readiness.issue !== undefined))
34
- return `${status.displayName} can't be used yet: ${readiness.issue ?? "it isn't installed on this computer."} Fix it in Settings → Providers, then press Mark ready again.`;
34
+ return `${status.displayName} can't be used yet: ${readiness.issue ?? "it isn't installed on this computer."} Fix it in Settings → Keys & services, then press Approve for schedules again.`;
35
35
  return null;
36
36
  },
37
37
  };
@@ -26,7 +26,7 @@ export function designFields(doc) {
26
26
  const slotSentences = {
27
27
  unclosed: "has a {{ without its }}",
28
28
  empty: "has an empty {{}}",
29
- nested: "has a { or } inside a {{field}}",
29
+ nested: "has a { or } inside a {{keyword}}",
30
30
  };
31
31
  export function fieldProblems(doc) {
32
32
  const problems = [];
@@ -36,7 +36,7 @@ export function fieldProblems(doc) {
36
36
  kind: "slot",
37
37
  where: body.where,
38
38
  error: error.kind,
39
- sentence: `The ${body.where.replace(/^the /, "")} ${slotSentences[error.kind]}. Fix it so every field reads {{Name}}.`,
39
+ sentence: `The ${body.where.replace(/^the /, "")} ${slotSentences[error.kind]}. Fix it so every keyword reads {{Name}}.`,
40
40
  });
41
41
  const names = designFields(doc);
42
42
  for (const name of names)
@@ -51,7 +51,7 @@ export function fieldProblems(doc) {
51
51
  problems.push({
52
52
  kind: "date-field",
53
53
  name: dateField,
54
- sentence: `The schedule reads each row's date from {{${dateField}}}, but no text uses it. Put {{${dateField}}} in a text block, or choose another date field under Schedule.`,
54
+ sentence: `The schedule reads each row's date from {{${dateField}}}, but no text uses it. Put {{${dateField}}} in a text block, or choose another date keyword under Schedule.`,
55
55
  });
56
56
  return problems;
57
57
  }
@@ -64,7 +64,7 @@ export function missingSentence(missing) {
64
64
  const named = list.length === 1
65
65
  ? `${list[0] ?? ""} has`
66
66
  : `${list.slice(0, -1).join(", ")} and ${list.at(-1) ?? ""} have`;
67
- return `${named} no value. Fill ${list.length === 1 ? "it" : "them"} in under Fields, then press Make.`;
67
+ return `${named} no value. Fill ${list.length === 1 ? "it" : "them"} in under Keywords, then press Make.`;
68
68
  }
69
69
  // Every text with its fields filled. Values are inserted verbatim, never read as fields.
70
70
  export function fillDocument(doc, values) {
@@ -33,8 +33,10 @@ export async function contextChecks(doc, templateId, ctx) {
33
33
  checks.push({
34
34
  id: "provider",
35
35
  ok: problem === null,
36
- sentence: problem ?? "The image provider is ready.",
37
- fix: problem === null ? null : { label: "Open Providers", href: "/settings?section=providers" },
36
+ sentence: problem ?? "The image service is ready.",
37
+ fix: problem === null
38
+ ? null
39
+ : { label: "Open Keys & services", href: "/settings?section=providers" },
38
40
  });
39
41
  }
40
42
  else {
@@ -56,7 +58,7 @@ export async function contextChecks(doc, templateId, ctx) {
56
58
  ok: used,
57
59
  sentence: used
58
60
  ? `Each row's date is read from {{${schedule.dateField}}}.`
59
- : `The schedule reads each date from {{${schedule.dateField}}}, but the template doesn't use that field. Choose a field it uses under Schedule.`,
61
+ : `The schedule reads each date from {{${schedule.dateField}}}, but the template doesn't use that keyword. Choose a keyword it uses under Schedule.`,
60
62
  fix: used ? null : at("schedule"),
61
63
  });
62
64
  }
@@ -107,12 +109,12 @@ export async function contextChecks(doc, templateId, ctx) {
107
109
  });
108
110
  return checks;
109
111
  }
110
- // Mark ready asks for the version's own sample, made, as a project template's studio does: the
112
+ // Approve for schedules asks for the version's own sample, made, as Edit template does: the
111
113
  // person has seen what a schedule will make before it makes it.
112
114
  export function sampleCheck(db, templateId, version) {
113
115
  const row = sampleRow(db, templateId);
114
116
  const made = row?.state === "done" && row.version === version;
115
- const how = "Make its sample, look it over beside the settings, then press Mark ready.";
117
+ const how = "Make its sample, look it over beside the settings, then press Approve for schedules.";
116
118
  return {
117
119
  id: "sample",
118
120
  ok: made,
@@ -9,7 +9,7 @@ export function prepare(db, designId, format) {
9
9
  return {
10
10
  ok: false,
11
11
  reason: "not-found",
12
- message: "This design no longer exists. Open Library → Designs to choose another.",
12
+ message: "This design no longer exists. Go back to Designs to choose another.",
13
13
  };
14
14
  const parsed = designDocumentSchema.safeParse(design.document);
15
15
  if (!parsed.success)
@@ -13,7 +13,7 @@ const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
13
13
  export const designValuesSchema = z
14
14
  .record(z.string().min(1).max(100), z.string().max(2000))
15
15
  .refine((values) => Object.keys(values).length <= 100, {
16
- message: "A design has at most 100 fields.",
16
+ message: "A design has at most 100 keywords.",
17
17
  });
18
18
  export const createDesignSchema = z
19
19
  .object({
@@ -14,7 +14,7 @@ const noProvider = {
14
14
  const notFound = {
15
15
  ok: false,
16
16
  reason: "not-found",
17
- message: "This design no longer exists. Open Library → Designs to choose another.",
17
+ message: "This design no longer exists. Go back to Designs to choose another.",
18
18
  };
19
19
  export function createDesignRunner(deps) {
20
20
  const calls = createDesignCalls(deps);
@@ -1,4 +1,5 @@
1
1
  import { transact } from "../../kernel/db/tx.js";
2
+ import { plansSwitch } from "../template-studio/budget.js";
2
3
  import { budgetSentence } from "./budget.js";
3
4
  import { missingValues } from "./fields.js";
4
5
  import { costChanged, costChangedMessage } from "./quote.js";
@@ -56,7 +57,7 @@ export function createDesignSamples(deps) {
56
57
  const spent = sampleSpentToday(db, now(), deps.timeZone()) + sampleReserved(db, templateId);
57
58
  const budget = deps.budget();
58
59
  return quote.rows.some((one) => one.onPlan === true) && !deps.plans()
59
- ? "Waiting: this sample's picture runs on your CLI plan, and samples may not use CLI plans. Turn on Samples may use my CLI plans in Settings → Template studio, or press Make sample to make it anyway."
60
+ ? `Waiting: this sample's picture runs on your Claude Code or Codex plan, and samples may not use it. Turn on ${plansSwitch} in Settings → Production defaults, or press Make sample to make it anyway.`
60
61
  : quote.unknown > 0
61
62
  ? "Waiting: the price of this sample's picture isn't known, so Slopify didn't make it. Press Make sample to make it anyway."
62
63
  : quote.high > holdSlack(acknowledgedHigh)
@@ -16,7 +16,7 @@ export const reusedMessage = "This request id was already used for a different c
16
16
  const notFound = {
17
17
  ok: false,
18
18
  reason: "not-found",
19
- message: "This design no longer exists. Open Library → Designs to choose another.",
19
+ message: "This design no longer exists. Go back to Designs to choose another.",
20
20
  };
21
21
  export function mutationHash(input) {
22
22
  return createHash("sha256").update(JSON.stringify(input)).digest("hex");
@@ -21,14 +21,14 @@ export function documentChecks(doc, templateId) {
21
21
  {
22
22
  id: "fields",
23
23
  ok: problems.length === 0,
24
- sentence: problems[0]?.sentence ?? "Every field is written as {{Name}}.",
24
+ sentence: problems[0]?.sentence ?? "Every keyword is written as {{Name}}.",
25
25
  fix: problems.length === 0 ? null : at("text"),
26
26
  },
27
27
  {
28
28
  id: "samples",
29
29
  ok: missing.length === 0,
30
30
  sentence: missing.length === 0
31
- ? "Every field has a sample value."
31
+ ? "Every keyword has a sample value."
32
32
  : `${missing.map((name) => `{{${name}}}`).join(", ")} ${missing.length === 1 ? "has" : "have"} no sample value. Fill ${missing.length === 1 ? "it" : "them"} in under Text → Sample values.`,
33
33
  fix: missing.length === 0 ? null : at("text"),
34
34
  },
@@ -27,7 +27,7 @@ export const makeFromTemplateSchema = z
27
27
  const notFound = {
28
28
  ok: false,
29
29
  reason: "not-found",
30
- message: "This design template no longer exists. Open Library → Designs → Templates to choose another.",
30
+ message: "This design template no longer exists. Open Templates → Poster templates to choose another.",
31
31
  };
32
32
  // What one piece made from this document costs: its artwork, or nothing when the template
33
33
  // keeps its own picture.
@@ -53,7 +53,7 @@ export function makeFromTemplate(deps, templateId, raw, origin = {}) {
53
53
  return {
54
54
  ok: false,
55
55
  reason: "invalid",
56
- message: `The piece can't be made: ${parsed.error.issues[0]?.message ?? "a field is missing"}.`,
56
+ message: `The poster can't be made: ${parsed.error.issues[0]?.message ?? "a keyword is missing"}.`,
57
57
  };
58
58
  const input = parsed.data;
59
59
  const template = getTemplate(deps.service.db, templateId);
@@ -78,7 +78,7 @@ export function makeFromTemplate(deps, templateId, raw, origin = {}) {
78
78
  return {
79
79
  ok: false,
80
80
  reason: "cost-changed",
81
- message: costChangedMessage(quote, "this piece"),
81
+ message: costChangedMessage(quote, "this poster"),
82
82
  current: quote,
83
83
  };
84
84
  const own = doc.picture.source === "picture" ? doc.picture.sha256 : undefined;
@@ -109,7 +109,7 @@ export function listTemplates(db) {
109
109
  const notFound = {
110
110
  ok: false,
111
111
  reason: "not-found",
112
- message: "This design template no longer exists. Open Library → Designs → Templates to choose another.",
112
+ message: "This design template no longer exists. Open Templates → Poster templates to choose another.",
113
113
  };
114
114
  function invalid(error) {
115
115
  return {
@@ -187,7 +187,7 @@ export function saveTemplate(deps, templateId, raw) {
187
187
  export async function markTemplateReady(deps, templateId, raw, more = async () => []) {
188
188
  const parsed = markReadySchema.safeParse(raw);
189
189
  if (!parsed.success)
190
- return { ok: false, reason: "invalid", message: "Say which version to mark ready." };
190
+ return { ok: false, reason: "invalid", message: "Say which version to approve for schedules." };
191
191
  if (getTemplate(deps.db, templateId) === undefined)
192
192
  return notFound;
193
193
  const revision = templateRevision(deps.db, templateId, parsed.data.version);
@@ -210,7 +210,7 @@ export async function markTemplateReady(deps, templateId, raw, more = async () =
210
210
  return {
211
211
  ok: false,
212
212
  reason: "not-ready",
213
- message: `Version ${String(parsed.data.version)} can't be marked ready yet: ${failed.map((check) => check.sentence).join(" ")}`,
213
+ message: `Version ${String(parsed.data.version)} can't be approved for schedules yet: ${failed.map((check) => check.sentence).join(" ")}`,
214
214
  current: failed,
215
215
  };
216
216
  const template = getTemplate(deps.db, templateId);
@@ -33,7 +33,7 @@ export function readDocumentAssets() {
33
33
  };
34
34
  }
35
35
  catch (error) {
36
- throw new Error(`Slopify couldn't read the fonts and page texture it makes documents with (${errorText(error)}). Its installation is incomplete: reinstall or update Slopify, then use Try again on Document.`, { cause: error });
36
+ throw new Error(`Slopify couldn't read the fonts and page texture it makes documents with (${errorText(error)}). Its installation is incomplete: reinstall or update Slopify, then use Try again on PDF.`, { cause: error });
37
37
  }
38
38
  }
39
39
  export function registerFonts(doc, fonts) {
@@ -1,6 +1,7 @@
1
- // The built-in looks a project's PDF can be given. Browser-safe: Play, Edit project and the
2
- // server all offer the same names from here.
3
- export const documentThemes = ["plain"];
1
+ // The ready looks a project's PDF can be given, so a PDF looks right without a theme of one's
2
+ // own: Plain, Book (book type on parchment) and Modern (a sans-serif on white). Browser-safe:
3
+ // Create, Edit template, Edit settings and the server all offer the same names from here.
4
+ export const documentThemes = ["plain", "book", "modern"];
4
5
  // DiceMaster was built in before 2.3.0 and is one brand's look, not Slopify's. A project
5
6
  // saved with it keeps it (see legacy-dicemaster.ts), so the name is still read everywhere a
6
7
  // config is parsed, but it is never offered.
@@ -11,6 +12,8 @@ export const documentThemeNames = [...documentThemes, legacyDocumentTheme];
11
12
  export const defaultDocumentTheme = "plain";
12
13
  export const documentThemeLabels = {
13
14
  plain: "Plain",
15
+ book: "Book",
16
+ modern: "Modern",
14
17
  dicemaster: "DiceMaster",
15
18
  };
16
19
  // The built-in a saved project config is drawn with. A project saved before the Document stage
@@ -34,7 +37,7 @@ export const documentThemeNameMax = 80;
34
37
  export function documentThemeNameProblems(name) {
35
38
  const trimmed = name.trim();
36
39
  if (trimmed === "")
37
- return [{ field: "name", message: "Give the theme a name." }];
40
+ return [{ field: "name", message: "Give the look a name." }];
38
41
  if (trimmed.length > documentThemeNameMax)
39
42
  return [
40
43
  {
@@ -1,4 +1,5 @@
1
1
  import { jsPDF } from "jspdf";
2
+ import { editSettings } from "../../kernel/ports/settings-places.js";
2
3
  import { runsText } from "./blocks.js";
3
4
  import { drawLine, wrapRuns } from "./flow.js";
4
5
  import { faceWidth, registerFonts, useFace } from "./fonts.js";
@@ -10,7 +11,7 @@ export function renderDocument(input) {
10
11
  const text = documentText(input.articleMarkdown, input.researchNotes);
11
12
  const blocks = withoutTitle(text.blocks, input.title);
12
13
  if (blocks.length === 0)
13
- throw new Error("The article has no text to put in the document. Write or regenerate the article (Edit project → Article), then use Try again on Document.");
14
+ throw new Error(`The article has no text to put in the PDF. Write it in ${editSettings.article}, or press Write the article again in the project's Article section, then use Try again on PDF.`);
14
15
  const words = blocks
15
16
  .map((block) => block.kind === "heading" ? block.text : "runs" in block ? runsText(block.runs) : "")
16
17
  .join(" ")
@@ -8,7 +8,7 @@ const mm = (min, max) => {
8
8
  const range = { error: `Use a value from ${String(min)} to ${String(max)}.` };
9
9
  return z.number({ error: "Enter a number." }).finite(range).min(min, range).max(max, range);
10
10
  };
11
- const color = z.string().regex(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i, "Use a colour like #8c1e14.");
11
+ const color = z.string().regex(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i, "Use a color like #8c1e14.");
12
12
  const line = z.string().max(300, { error: "Keep this under 300 characters." });
13
13
  const nullableLine = line.nullable();
14
14
  const url = z.string().max(500, { error: "Keep this under 500 characters." }).nullable();
@@ -9,8 +9,11 @@ const cinzel = (style, letterSpacing) => ({
9
9
  style,
10
10
  letterSpacing,
11
11
  });
12
- // The one built-in: an unbranded flat page. Its layout is the one Slopify has always drawn,
13
- // written out in full rather than as changes to another theme.
12
+ const literata = (style) => ({ family: "Literata", style, letterSpacing: 0 });
13
+ const sans = (style) => ({ family: "helvetica", style, letterSpacing: 0 });
14
+ // The first built-in, and the default: an unbranded flat page. Its layout is the one Slopify
15
+ // has always drawn, written out in full rather than as changes to another theme. The other
16
+ // built-ins are changes to it.
14
17
  const plain = {
15
18
  page: { format: "a4", margin: 22, contentTop: 32 },
16
19
  background: { image: null, color: "#fdfaf3" },
@@ -91,10 +94,53 @@ const plain = {
91
94
  closing: null,
92
95
  },
93
96
  };
97
+ // Book: the article set like a novel, in Literata on the parchment texture, with Plain's Cinzel
98
+ // headings and drop caps in a warm brown.
99
+ const book = {
100
+ background: { image: "parchment", color: "#f5ebd7" },
101
+ colors: { heading: "#5a3214", text: "#231a12", muted: "#5e5246", faint: "#857767" },
102
+ fonts: {
103
+ body: literata("normal"),
104
+ strong: literata("bold"),
105
+ emphasis: literata("italic"),
106
+ footer: literata("italic"),
107
+ },
108
+ sizes: { body: 11 },
109
+ spacing: { bodyLine: 6.5, paragraphGap: 0.6 },
110
+ // A paragraph of a line or two starts plainly: a three-line initial beside it looks lost.
111
+ dropCap: { minLength: 120 },
112
+ };
113
+ // Modern: a sans-serif on a white page, blue headings and no drop caps, like a report.
114
+ const modern = {
115
+ background: { image: null, color: "#ffffff" },
116
+ colors: { heading: "#1d4e89", text: "#1c1c1c", muted: "#5a5a5a", faint: "#8a8a8a" },
117
+ fonts: {
118
+ body: sans("normal"),
119
+ strong: sans("bold"),
120
+ emphasis: sans("italic"),
121
+ heading: sans("bold"),
122
+ dramatic: sans("bold"),
123
+ decorative: sans("normal"),
124
+ dropCap: sans("bold"),
125
+ footer: sans("normal"),
126
+ },
127
+ sizes: { brand: 28, title: 24, section: 18, heading: 15, subheading: 12, body: 10.5 },
128
+ spacing: { bodyLine: 6, headingLine: 9, subheadingLine: 7, paragraphGap: 0.7 },
129
+ dropCap: { enabled: false },
130
+ };
94
131
  // A built-in by name. "dicemaster" is still read, from projects saved while it was built in,
95
132
  // and gives exactly the values it gave then.
96
133
  export function builtInTheme(name) {
97
- return resolveTheme({}, name === legacyDocumentTheme ? legacyDiceMasterTheme : plain);
134
+ switch (name) {
135
+ case "book":
136
+ return resolveTheme(book, plain);
137
+ case "modern":
138
+ return resolveTheme(modern, plain);
139
+ case legacyDocumentTheme:
140
+ return resolveTheme({}, legacyDiceMasterTheme);
141
+ default:
142
+ return resolveTheme({}, plain);
143
+ }
98
144
  }
99
145
  // The theme a project's document is drawn with: its copy of a Library theme, or a built-in.
100
146
  export function resolvedDocumentTheme(settings) {
@@ -53,7 +53,7 @@ export async function summarizeEpisode(deps, projectId, signal) {
53
53
  return "failed";
54
54
  deps.log.write("warn", "episode.memory", {
55
55
  projectId,
56
- detail: `The episode summary wasn't saved: ${errorText(error)}. The project is unaffected; only later episodes won't be reminded of this one. Check the project's text model in Settings → Providers.`,
56
+ detail: `The episode summary wasn't saved: ${errorText(error)}. The project is unaffected; only later episodes won't be reminded of this one. Check the service it writes with in Settings → Keys & services.`,
57
57
  });
58
58
  return "failed";
59
59
  }
@@ -82,7 +82,7 @@ async function summarize(deps, projectId, signal) {
82
82
  if (choice === undefined) {
83
83
  deps.log.write("info", "episode.memory", {
84
84
  projectId,
85
- detail: "No episode summary: this project has no text model chosen, so later episodes won't be reminded of it.",
85
+ detail: "No episode summary: this project has no AI model chosen, so later episodes won't be reminded of it.",
86
86
  });
87
87
  return "no-llm";
88
88
  }
@@ -42,14 +42,13 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
42
42
  const tts = draft.audio ?? { provider: "", model: "" };
43
43
  const image = draft.images ?? { provider: "", model: "" };
44
44
  const textNote = isLocalCliProvider(llm.provider)
45
- ? "Runs on your CLI plan, so it adds no charge; the API figure is what the same tokens would cost through the API."
45
+ ? "Runs on your subscription at no extra charge; the API figure is what the same work would cost through the API."
46
46
  : (data.llm.find((model) => model.provider === llm.provider &&
47
47
  model.id === llm.model &&
48
48
  model.enabled &&
49
- !model.deprecated)?.pricing.note ??
50
- "CLI subscription or model pricing is unavailable; usage may be billed by your account.");
49
+ !model.deprecated)?.pricing.note ?? "No price is listed for this model; your account may be billed for it.");
51
50
  const imageNote = isLocalCliProvider(image.provider)
52
- ? "Runs on your Codex plan, so it adds no charge; no API price is given for agent-drawn images."
51
+ ? "Runs on your Codex subscription at no extra charge."
53
52
  : (data.image.find((model) => model.provider === image.provider &&
54
53
  model.id === image.model &&
55
54
  model.enabled &&
@@ -98,15 +97,18 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
98
97
  code: draft.audio?.skipCode === true ? "skip" : "describe",
99
98
  language: draft.language,
100
99
  }));
101
- if (blocks === undefined)
100
+ // On a subscription the count does not change the price.
101
+ if (blocks === undefined && isLocalCliProvider(llm.provider))
102
+ local("Describing tables and figures", "One AI call per table, figure, equation or code block in the article, on your subscription at no extra charge.");
103
+ else if (blocks === undefined)
102
104
  requests.push({
103
105
  kind: "unknown",
104
- stage: "Narration descriptions",
105
- detail: "One LLM call per table, figure, equation or code block in the article; how many is known once the article is written.",
106
+ stage: "Describing tables and figures",
107
+ detail: "One AI call per table, figure, equation or code block in the article; how many is known once the article is written.",
106
108
  });
107
109
  else
108
110
  for (const block of blocks)
109
- text("Narration descriptions", describeMessages(block, rendered.narration ?? "", draft.language).reduce((n, message) => n + message.content.length, 0), 600, `${String(blocks.length)} LLM ${blocks.length === 1 ? "call" : "calls"}: one per table, figure, equation or code block.`);
111
+ text("Describing tables and figures", describeMessages(block, rendered.narration ?? "", draft.language).reduce((n, message) => n + message.content.length, 0), 600, `${String(blocks.length)} AI ${blocks.length === 1 ? "call" : "calls"}: one per table, figure, equation or code block.`);
110
112
  }
111
113
  if (draft.sources.audio === "generate") {
112
114
  if (usesNarrationPreparation(draft)) {
@@ -122,16 +124,16 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
122
124
  if (unknown)
123
125
  requests.push({
124
126
  kind: "unknown",
125
- stage: "Narration Preparation",
126
- detail: "One LLM call per future logical narration chunk and enabled entry; generated source length is not yet known.",
127
+ stage: "Narration preparation",
128
+ detail: "One AI call per narration piece and per intro or outro written for it; how many is known once the text is written.",
127
129
  });
128
130
  else
129
131
  for (const source of known)
130
- text("Narration Preparation", preparationMessages(rendered.narration ?? "", source, [], draft.language).reduce((n, message) => n + message.content.length, 0), 2400, `${known.length} LLM calls: one per logical narration chunk or entry.`);
132
+ text("Narration preparation", preparationMessages(rendered.narration ?? "", source, [], draft.language).reduce((n, message) => n + message.content.length, 0), 2400, `${known.length} AI calls: one per narration piece or intro or outro.`);
131
133
  requests.push({
132
134
  kind: "unknown",
133
- stage: "Delivery cue overhead",
134
- detail: "TTS character charges include delivery tags; their length is known after preparation.",
135
+ stage: "Delivery cues",
136
+ detail: "The voice service charges per character, delivery tags included; their length is known after preparation.",
135
137
  });
136
138
  }
137
139
  // A template studio sample says only the narration's opening (`template-studio/scope.ts`).
@@ -155,9 +157,9 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
155
157
  if (extras + llmExtras > 0)
156
158
  requests.push({ ...choice, kind: "tts-estimate", characters: extras + llmExtras });
157
159
  if (draft.voices?.source === "attribute")
158
- text("Speaker split", articleChars + 1500, Math.round(articleChars * 1.1), "One LLM call hands the text's passages to the speakers.");
160
+ text("Speaker split", articleChars + 1500, Math.round(articleChars * 1.1), "One AI call hands the text's passages to the speakers.");
159
161
  if (draft.voices?.source === "adapt")
160
- text("Conversation script", articleChars + 1500, Math.round(articleChars * 1.2), "One LLM call rewrites the text as a conversation between the speakers; the article stays as written.");
162
+ text("Conversation script", articleChars + 1500, Math.round(articleChars * 1.2), "One AI call rewrites the text as a conversation between the speakers; the article stays as written.");
161
163
  }
162
164
  else
163
165
  requests.push(generatedArticle || llmExtras > 0
@@ -230,7 +232,11 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
230
232
  local("Thumbnail", "Provided or off.");
231
233
  if (draft.sources.thumbnail === "prompt_by_llm")
232
234
  text("Thumbnail prompt", promptChars + articleChars, 1200);
233
- local("Export / subtitles", "Local processing; no API fee.");
235
+ // Putting it together on this computer: the video (with its captions), or the sound alone.
236
+ if (draft.sources.video === "generate")
237
+ local("Video", "Made on this computer; no API fee.");
238
+ else if (draft.sources.audio !== "off")
239
+ local("Audio export", "Made on this computer; no API fee.");
234
240
  // The timed transcript is the narration with a time before every passage of about 20 s,
235
241
  // plus the fixed rules of the answer.
236
242
  if (usesYoutubeDescription(draft))
@@ -238,13 +244,13 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
238
244
  ? rendered.description.length
239
245
  : defaultDescriptionPrompt.length) +
240
246
  Math.round(articleChars * 1.05) +
241
- 1500, 2400, "One LLM call on the timed transcript.");
247
+ 1500, 2400, "One AI call on the timed transcript.");
242
248
  // Each site whose post is written by a post prompt: one call that reads the article.
243
249
  for (const platform of socialPlatforms) {
244
250
  if (draft.social?.[platform]?.text !== "prompt")
245
251
  continue;
246
252
  const own = rendered[`social.${platform}`]?.trim() ?? "";
247
- text("Posts", (own === "" ? defaultPostPrompt.length : own.length) + articleChars + 400, 600, "One LLM call per site whose post a post prompt writes, reading the article.");
253
+ text("Posts", (own === "" ? defaultPostPrompt.length : own.length) + articleChars + 400, 600, "One AI call per site whose post a post prompt writes, reading the article.");
248
254
  }
249
255
  // Shorts: one call picks the clips from the numbered transcript, one per clip writes its
250
256
  // image prompts, and the images are charged at the most the step can ask for, every clip
@@ -257,12 +263,12 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
257
263
  imageSecondsProblem(draft.imageSeconds) === undefined) {
258
264
  text("Shorts", (rendered.shorts?.trim() ? rendered.shorts.length : defaultShortsPrompt.length) +
259
265
  Math.round(articleChars * 1.15) +
260
- 1500, 400 * shorts.count, "One LLM call on the numbered transcript picks the clips.");
266
+ 1500, 400 * shorts.count, "One AI call on the numbered transcript picks the clips.");
261
267
  const style = rendered.shortsImage?.trim()
262
268
  ? rendered.shortsImage.length
263
269
  : defaultShortsImagePrompt.length;
264
270
  for (let clip = 0; clip < shorts.count; clip++)
265
- text("Shorts", style + 3000, 2400, "One LLM call per short writes its image prompts.");
271
+ text("Shorts", style + 3000, 2400, "One AI call per short writes its image prompts.");
266
272
  const count = shortsImageUpperBound(shorts, draft.imageSeconds);
267
273
  for (let index = 0; index < count; index++)
268
274
  requests.push({
@@ -298,7 +304,7 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
298
304
  const reviews = draft.reviews;
299
305
  if (reviews !== undefined && reviewedByAgent(reviews)) {
300
306
  if (activeReviewStages(draft).length > 0)
301
- local("Reviews", "An external agent answers each review through MCP; no API fee.");
307
+ local("Reviews", "Your own AI agent answers each check; no API fee.");
302
308
  }
303
309
  else if (reviews !== undefined)
304
310
  for (const stage of activeReviewStages(draft)) {
@@ -324,7 +330,7 @@ export function estimateRun(draft, rendered, expectedWords, catalogue) {
324
330
  });
325
331
  }
326
332
  if (sourceOf(draft.sources, "document") === "generate")
327
- local("Document", "Laid out locally from the article; no API fee.");
333
+ local("PDF", "Laid out on this computer from the article; no API fee.");
328
334
  return {
329
335
  ...groupEstimateRows(estimateRequests(requests, data)),
330
336
  expectedWords,
@@ -38,8 +38,8 @@ export function estimateRequests(requests, catalogue) {
38
38
  catalogueDate: catalogue.updatedAt,
39
39
  assumptions: [
40
40
  "Planning estimate, not a spending limit. Generated lengths use a ±50% range; actual output can exceed it.",
41
- "Assumes roughly 6 characters per word, 4 characters per token, and 150 spoken words per minute.",
42
- "Retries, extra reasoning tokens, web tools, taxes, discounts and included credits are excluded. Unknown charges are not counted as zero.",
41
+ "Assumes about 6 letters per word and 150 spoken words a minute.",
42
+ "Retries, extra thinking, web search, taxes, discounts and included credits are not counted. A charge with no listed price is not counted as zero.",
43
43
  ],
44
44
  };
45
45
  }
@@ -99,8 +99,8 @@ function onPlan(request, catalogue) {
99
99
  high: 0,
100
100
  detail: request.detail ??
101
101
  (amount === null
102
- ? "Runs on your CLI plan, so it adds no charge; no API price is listed for this model."
103
- : `Runs on your CLI plan, so it adds no charge; priced as ${api?.name ?? "the API model"} through the API.`),
102
+ ? "Runs on your subscription at no extra charge; no API price is listed for this model."
103
+ : `Runs on your subscription at no extra charge; through the API it would cost what ${api?.name ?? "the API model"} costs.`),
104
104
  onPlan: true,
105
105
  apiLow: amount === null ? null : amount * 0.5,
106
106
  apiHigh: amount === null ? null : amount * 1.5,
@@ -35,7 +35,9 @@ export function fixFor(step) {
35
35
  const name = nameOf(step.provider);
36
36
  return {
37
37
  kind: "provider-settings",
38
- label: name === undefined ? "Open Settings → Providers" : `Open Settings → Providers → ${name}`,
38
+ label: name === undefined
39
+ ? "Open Settings → Keys & services"
40
+ : `Open Settings → Keys & services → ${name}`,
39
41
  provider: step.provider ?? "",
40
42
  };
41
43
  }
@@ -3,6 +3,7 @@ import { lstat, readdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { isUnreachablePath } from "../../kernel/errors.js";
6
+ import { editSettings } from "../../kernel/ports/settings-places.js";
6
7
  import { discoverSystemFonts } from "./discovery.js";
7
8
  import { readFontFile } from "./files.js";
8
9
  import { fontSummary } from "./model.js";
@@ -55,7 +56,7 @@ export async function resolveBoldFont(paths, id) {
55
56
  };
56
57
  }
57
58
  export function missingFont() {
58
- return new Error("The chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another font in Edit project → Subtitles, then try again.", {
59
+ return new Error(`The chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another font in ${editSettings.captions}, then try again.`, {
59
60
  cause: "font-not-found",
60
61
  });
61
62
  }
@@ -1,5 +1,6 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { languageInfo, languageLetters } from "../../kernel/ports/languages.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { resolveBoldFont, resolveFont } from "./catalog.js";
4
5
  import { discoverSystemFonts } from "./discovery.js";
5
6
  import { readFontFile } from "./files.js";
@@ -45,7 +46,7 @@ export async function captionFont(deps, fontId, language) {
45
46
  for (const font of await deps.system())
46
47
  if (await covers(font, letters))
47
48
  return font;
48
- throw new Error(`The caption font ${chosen.name} has no ${info.name} letters, and Slopify doesn't ship a font for ${info.name} (they are 10-20 MB). In Edit project → Subtitles, use Upload font in the caption font picker to add a font that has them (for example Noto Sans ${info.code === "ja" ? "JP" : info.code === "ko" ? "KR" : "SC"} from fonts.google.com), choose it there, then Try again.`);
49
+ throw new Error(`The caption font ${chosen.name} has no ${info.name} letters, and Slopify doesn't ship a font for ${info.name} (they are 10-20 MB). In ${editSettings.captions}, use Upload font in the caption font picker to add a font that has them (for example Noto Sans ${info.code === "ja" ? "JP" : info.code === "ko" ? "KR" : "SC"} from fonts.google.com), choose it there, then Try again.`);
49
50
  }
50
51
  // The same for text drawn bold (the shorts' captions): the chosen font's bold face when it has
51
52
  // the letters, else the fallback, which the renderer emboldens.
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { jsonBetween } from "../../kernel/json-answer.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { appearanceKeyword, detectSlots, render } from "../admission/substitute.js";
4
5
  // How things look, looked up rather than guessed: when an image or thumbnail prompt has
5
6
  // `{{Appearance}}`, one call to the project's AI model searches the web for how the video's
@@ -44,7 +45,7 @@ export function appearanceMessages(brief) {
44
45
  ];
45
46
  }
46
47
  export function checkAppearance(text) {
47
- const fix = "Use Try again; if it keeps happening, choose another model in the Providers section of Edit project.";
48
+ const fix = `Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`;
48
49
  const parsed = appearanceSchema.safeParse(jsonBetween(text, "{", "}"));
49
50
  if (!parsed.success)
50
51
  return { ok: false, reason: `The AI model didn't answer with the looks it looked up. ${fix}` };
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { jsonBetween } from "../../kernel/json-answer.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { detectSlots, render, sceneKeyword } from "../admission/substitute.js";
4
5
  // Scenes from the article (Images → Scenes from the article): each image is drawn from its
5
6
  // own scene instead of the same text every time. One call to the project's AI model reads the
@@ -197,7 +198,7 @@ const sceneAnswer = z.array(z.union([
197
198
  ]));
198
199
  // A list of scenes, or of scenes with their setting and mood (`details`, only then present).
199
200
  export function checkScenes(text, count) {
200
- const fix = "Use Try again; if it keeps happening, choose another model in the Providers section of Edit project.";
201
+ const fix = `Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`;
201
202
  const parsed = sceneAnswer.safeParse(jsonBetween(text, "[", "]"));
202
203
  if (!parsed.success)
203
204
  return {