@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
@@ -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 {
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { editSettings } from "../../kernel/ports/settings-places.js";
2
3
  import { keywordSpecs } from "./specs.js";
3
4
  // The keyword digest: one call to the project's AI model reads the article once and writes every
4
5
  // digest keyword (`{{Slopify_Summary}}`, `{{Slopify_Hook}}`…), so a run that uses several pays for
@@ -39,7 +40,7 @@ const digestSchema = z.object({
39
40
  tags: listOrText,
40
41
  });
41
42
  export function checkDigest(text) {
42
- const fix = "Use Try again; if it keeps happening, choose another model in the Providers section of Edit project.";
43
+ const fix = `Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`;
43
44
  const parsed = digestSchema.safeParse(jsonObjectOf(text));
44
45
  if (!parsed.success)
45
46
  return {
@@ -167,7 +167,7 @@ export function placedProblems(bodies, values) {
167
167
  field,
168
168
  message: place === "title"
169
169
  ? problem.message
170
- : `${problem.message} Change the prompt in Library, or pick another one.`,
170
+ : `${problem.message} Change the prompt in Prompts, or pick another one.`,
171
171
  });
172
172
  }
173
173
  return problems;
@@ -180,7 +180,7 @@ export function textEntryProblems(body, category) {
180
180
  ? [
181
181
  {
182
182
  field: category,
183
- message: `{{Slopify_Release_Date}} can't be used in a Text ${category}: Text is narrated as written, and the release date is only filled in when a step asks the AI model. Set this ${category}'s Mode to LLM in Library → Intros & Outros, or remove the keyword.`,
183
+ message: `{{Slopify_Release_Date}} can't be used in an ${category} that says these exact words: those are narrated as written, and the release date is only filled in when a step asks the AI model. In Prompts → Intros and outros, set this ${category}'s What the narrator reads to Let the AI write it from these instructions, or remove the keyword.`,
184
184
  },
185
185
  ]
186
186
  : [];
@@ -38,7 +38,7 @@ export const keywordSpecs = [
38
38
  {
39
39
  name: "Slopify_Series",
40
40
  filler: "facts",
41
- fills: "The channel's series brief from Channels → Edit. A line with an empty brief is left out.",
41
+ fills: "The channel's series brief, from the channel's page under Name and look. A line with an empty brief is left out.",
42
42
  example: "Calm histories of how things were made, one invention at a time.",
43
43
  available: atStart,
44
44
  places: everywhere,
@@ -88,7 +88,7 @@ export const keywordSpecs = [
88
88
  {
89
89
  name: "Slopify_Release_Date",
90
90
  filler: "release",
91
- fills: "The release date from the posting plan. A line with no date is left out; moving the date later doesn't remake anything.",
91
+ fills: "The release date from the posting plan. A line with no date is left out; moving the date later makes nothing again.",
92
92
  example: "Thursday 22 October 2026",
93
93
  available: "When the step runs.",
94
94
  places: everywhere,
@@ -138,8 +138,8 @@ export const keywordSpecs = [
138
138
  {
139
139
  name: "Slopify_Brand",
140
140
  filler: "facts",
141
- fills: "The brand kit's caption and title colours and font names. A line with an empty kit is left out.",
142
- example: "Colours: #f5c542, #101010. Fonts: Barlow Condensed",
141
+ fills: "The brand kit's caption and title colors and font names. A line with an empty kit is left out.",
142
+ example: "Colors: #f5c542, #101010. Fonts: Barlow Condensed",
143
143
  available: atStart,
144
144
  places: pictures,
145
145
  cost: "free",
@@ -229,7 +229,7 @@ export const keywordSpecs = [
229
229
  {
230
230
  name: "Slopify_Mood",
231
231
  filler: "scenes",
232
- fills: "The feeling of this picture's moment, for its light and colour.",
232
+ fills: "The feeling of this picture's moment, for its light and color.",
233
233
  example: "tense",
234
234
  available: sceneAvailable,
235
235
  places: pictures,
@@ -37,7 +37,7 @@ function brandText(brand) {
37
37
  if (brand === undefined)
38
38
  return "";
39
39
  const parts = [
40
- ...(brand.colours.length === 0 ? [] : [`Colours: ${brand.colours.join(", ")}.`]),
40
+ ...(brand.colours.length === 0 ? [] : [`Colors: ${brand.colours.join(", ")}.`]),
41
41
  ...(brand.fonts.length === 0 ? [] : [`Fonts: ${brand.fonts.join(", ")}.`]),
42
42
  ];
43
43
  return parts.join(" ");
@@ -17,7 +17,7 @@ const itemsMax = 10_000;
17
17
  const sectionWords = {
18
18
  prompts: "prompts",
19
19
  entries: "intros or outros",
20
- documentThemes: "PDF themes",
20
+ documentThemes: "PDF looks",
21
21
  aliases: "narration aliases",
22
22
  };
23
23
  // Only what the editor saves travels: ids, keywords and dates are the install's own.
@@ -93,7 +93,7 @@ export function readLibraryFile(text, section) {
93
93
  if (!top.success)
94
94
  return {
95
95
  ok: false,
96
- message: `it isn't a Slopify library file. Pick a .json file made by Export on a Library tab.`,
96
+ message: `it isn't a Slopify library file. Pick a .json file made by Export on Prompts, the PDF looks or Pronunciation.`,
97
97
  };
98
98
  if (top.data.version > libraryFileVersion)
99
99
  return {
@@ -104,7 +104,7 @@ export function readLibraryFile(text, section) {
104
104
  if (!Array.isArray(list) || list.length === 0)
105
105
  return {
106
106
  ok: false,
107
- message: `it holds no ${what}. Import it on the Library tab it was exported from.`,
107
+ message: `it holds no ${what}. Import it on the page it was exported from.`,
108
108
  };
109
109
  if (list.length > itemsMax)
110
110
  return {
@@ -1,5 +1,6 @@
1
1
  import { renameSync, rmSync } from "node:fs";
2
2
  import { z } from "zod";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { runFfmpeg } from "../video/ffmpeg.js";
4
5
  // ceiling: below -70 LUFS (the filter's absolute gate) or under 0.4 s (one measuring block)
5
6
  // there is nothing to measure: a piece that quiet or short is left as it is.
@@ -97,7 +98,7 @@ export async function measure(run, input, goal, chain = "") {
97
98
  });
98
99
  const measured = parseMeasured(stderr);
99
100
  if (measured === undefined)
100
- throw new Error("Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in Edit project → Pauses and volume, then use Download diagnostics in Settings and report it.");
101
+ throw new Error(`Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in ${editSettings.sound}, then use Download diagnostics in Settings and report it.`);
101
102
  return measured;
102
103
  }
103
104
  export async function measureFile(run, path, goal) {
@@ -9,10 +9,11 @@
9
9
  // The ambient bed and the shorts' music are mixed before the master and ducked by the levelled
10
10
  // voice, so they keep their place under it.
11
11
  //
12
- // Browser-safe: Play, Edit project and Settings convert the same numbers.
12
+ // Browser-safe: Create, Edit settings and Settings convert the same numbers.
13
13
  //
14
14
  // A run without the setting has no `loudness` at all, which is what every project saved before
15
15
  // it was, and every recipe leaves it out of its fingerprint then.
16
+ import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
16
17
  // YouTube and Spotify play everything back at about -14 LUFS, so a video mastered there is
17
18
  // neither turned down nor left quiet. Audiobook shops ask for about -18 to -20 LUFS with more
18
19
  // headroom, so the listening files sit lower.
@@ -87,7 +88,7 @@ export function loudnessProblem(lufs, target) {
87
88
  db >= loudnessDbMin - 1e-9 &&
88
89
  db <= loudnessDbMax + 1e-9
89
90
  ? undefined
90
- : `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (Play → Video and style, Edit project → Pauses and volume, or Settings → General).`;
91
+ : `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (${createRows.video}, ${editSettings.sound}, or Settings → Production defaults).`;
91
92
  }
92
93
  export function loudnessFields(settings) {
93
94
  if (settings === undefined)
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { isAbsolute, join, resolve as resolvePath } from "node:path";
4
+ import { editSettings } from "../../kernel/ports/settings-places.js";
4
5
  import { measurePiece } from "../narration/pauses.js";
5
6
  import { runFfmpeg } from "../video/ffmpeg.js";
6
7
  import { sentenceLevel } from "./sentence-level-model.js";
@@ -243,7 +244,7 @@ export async function meterFile(run, file) {
243
244
  else if (code === 0)
244
245
  resolve();
245
246
  else
246
- reject(new Error(`Slopify couldn't measure a narration piece's sentences (ffmpeg said: ${errors.trim().split("\n").at(-1) ?? "nothing"}). Try again; if it happens again, turn off Level each sentence in Edit project → Pauses and volume, then use Download diagnostics in Settings and report it.`));
247
+ reject(new Error(`Slopify couldn't measure a narration piece's sentences (ffmpeg said: ${errors.trim().split("\n").at(-1) ?? "nothing"}). Try again; if it happens again, turn off Level each sentence in ${editSettings.sound}, then use Download diagnostics in Settings and report it.`));
247
248
  });
248
249
  });
249
250
  return meter.readings();
@@ -110,3 +110,16 @@ export function dollarsText(dollars, unpriced = 0) {
110
110
  const more = `${String(unpriced)} model${unpriced === 1 ? "" : "s"} with no listed price`;
111
111
  return dollars <= 0 ? `${more} (price unknown)` : `${base}, plus ${more}`;
112
112
  }
113
+ // The same estimate as the end of a sentence, "this costs …": "about $0.04 at list price",
114
+ // and for models with no listed price "an unknown amount: 3 models have no listed price".
115
+ export function costPhrase(dollars, unpriced = 0) {
116
+ const base = dollars <= 0
117
+ ? "nothing"
118
+ : `${dollars < 0.01 ? "less than a cent" : `about $${dollars.toFixed(2)}`} at list price`;
119
+ if (unpriced === 0)
120
+ return base;
121
+ const models = `${String(unpriced)} model${unpriced === 1 ? "" : "s"}`;
122
+ return dollars <= 0
123
+ ? `an unknown amount: ${models} ${unpriced === 1 ? "has" : "have"} no listed price`
124
+ : `${base}, plus an unknown amount for ${models} with no listed price`;
125
+ }
@@ -3,7 +3,7 @@
3
3
  export const usageSlots = ["llm", "audio", "images", "animate"];
4
4
  export const usageKinds = ["template", "schedule", "draft", "project"];
5
5
  export const slotLabels = {
6
- llm: "Text model",
6
+ llm: "Writing model",
7
7
  audio: "Voice model",
8
8
  images: "Image model",
9
9
  animate: "Animation model",
@@ -94,7 +94,7 @@ export async function switchRetiredModel(deps, catalogue, request) {
94
94
  if (row.version !== row.head)
95
95
  return {
96
96
  ok: false,
97
- message: "This schedule runs an older version of its template. In Library → Schedules, choose Edit, pick the template again so it runs the latest version, then save.",
97
+ message: "This schedule runs an older version of its template. In Calendar → Schedules, choose Edit, pick the template again so it runs the latest version, then save.",
98
98
  };
99
99
  return switchTemplate(deps, catalogue, request, String(row.templateId));
100
100
  }
@@ -117,7 +117,7 @@ function switchTemplate(deps, catalogue, request, templateId) {
117
117
  return {
118
118
  ok: false,
119
119
  message: error.reason === "missing-prompt"
120
- ? `The template "${template.name}" uses a prompt or intro/outro that was deleted, so it cannot be saved. Open it in Library → Templates, choose another, and save.`
120
+ ? `The template "${template.name}" uses a prompt or intro/outro that was deleted, so it cannot be saved. Open it in Templates → Edit template, choose another, and save.`
121
121
  : changedMeanwhile,
122
122
  };
123
123
  }
@@ -230,7 +230,7 @@ async function switchProject(deps, catalogue, request) {
230
230
  ? " because the project changed meanwhile"
231
231
  : "fields" in saved && saved.fields?.[0] !== undefined
232
232
  ? ` (${saved.fields[0].message})`
233
- : ""}. Open the project, choose Edit project and change it under Providers.`,
233
+ : ""}. Open the project and change it in Edit settings.`,
234
234
  };
235
235
  }
236
236
  // "Switch all": every listed use that has a suggestion and no reason to hold back, one by one.