@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
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { fingerprint } from "../../kernel/runner/work.js";
3
+ import { actors } from "../agents/model.js";
4
+ import { deleteSetting, readJsonSetting, readSetting, writeSetting } from "../settings/repo.js";
5
+ import { libraryPronunciationsMax, parsePronunciationGlossary, } from "./pronunciation.js";
6
+ // Settings → Voices → Name pronunciations: one Pronunciation Glossary of your own, in the same
7
+ // `Term: /ipa/` lines an article's own glossary uses. Its entries come before the article's, so
8
+ // a name keeps the same pronunciation in every video however each article spells it out. A
9
+ // project takes a copy when it starts (and again when Edit project refreshes it), so a change
10
+ // here never alters a project's narration behind its back. Aliases on the Pronunciation page are
11
+ // the other half: they change the words, this changes the sounds.
12
+ export const libraryPronunciationsKey = "narration.pronunciations";
13
+ const savedKey = "narration.pronunciations.saved";
14
+ const savedRow = z
15
+ .object({ at: z.string().min(1).max(40), by: z.enum(actors) })
16
+ .strict()
17
+ .readonly();
18
+ export function readLibraryPronunciations(db) {
19
+ const saved = readJsonSetting(db, savedKey, savedRow, undefined);
20
+ return {
21
+ ...describeLibraryPronunciations(readSetting(db, libraryPronunciationsKey) ?? ""),
22
+ ...(saved === undefined ? {} : { savedAt: saved.at, savedBy: saved.by }),
23
+ };
24
+ }
25
+ export function describeLibraryPronunciations(text) {
26
+ const parsed = parsePronunciationGlossary(text);
27
+ return {
28
+ text,
29
+ terms: parsed.ok ? parsed.entries.length : 0,
30
+ version: versionOf(text),
31
+ ...(parsed.ok && parsed.skipped !== undefined ? { skipped: parsed.skipped } : {}),
32
+ ...(parsed.ok && parsed.unpronounced !== undefined
33
+ ? { unpronounced: parsed.unpronounced }
34
+ : {}),
35
+ };
36
+ }
37
+ // Short enough to pass around and read in a refusal, long enough never to collide.
38
+ export function versionOf(text) {
39
+ return fingerprint({ pronunciations: text.trim() }).slice(0, 16);
40
+ }
41
+ // `version` is the list the save was made from. It is required once a list exists: a save
42
+ // without it, or from an older one, is refused rather than replacing what somebody else wrote
43
+ // in another window or through an agent.
44
+ export function saveLibraryPronunciations(deps, text, version, by = "person") {
45
+ if (typeof text !== "string")
46
+ return {
47
+ ok: false,
48
+ reason: "refused",
49
+ detail: "Write the list as Term: /ipa/ lines, one name per line.",
50
+ };
51
+ if (text.length > libraryPronunciationsMax)
52
+ return {
53
+ ok: false,
54
+ reason: "refused",
55
+ detail: `Keep the list under ${String(libraryPronunciationsMax)} characters; it is ${String(text.length)} now. Remove the names you no longer narrate, then Save.`,
56
+ };
57
+ const current = readLibraryPronunciations(deps.db);
58
+ if (current.text !== "" && version !== current.version)
59
+ return {
60
+ ok: false,
61
+ reason: "conflict",
62
+ detail: conflictDetail(current, version),
63
+ current,
64
+ };
65
+ const trimmed = text.trim();
66
+ if (trimmed === "")
67
+ deleteSetting(deps.db, libraryPronunciationsKey);
68
+ else
69
+ writeSetting(deps.db, libraryPronunciationsKey, trimmed);
70
+ writeSetting(deps.db, savedKey, JSON.stringify({ at: deps.clock.now().toISOString(), by }));
71
+ return { ok: true, list: readLibraryPronunciations(deps.db) };
72
+ }
73
+ // Said to whoever sent the save: the person at the page, or an agent reading a refusal. It
74
+ // names what the list is now and the one step that keeps their own work.
75
+ function conflictDetail(current, version) {
76
+ const who = current.savedBy === "agent"
77
+ ? " by an agent"
78
+ : current.savedBy === "assistant"
79
+ ? " by the assistant"
80
+ : "";
81
+ const when = current.savedAt === undefined ? "" : ` at ${current.savedAt}`;
82
+ const names = `${String(current.terms)} ${current.terms === 1 ? "name" : "names"}`;
83
+ return version === undefined
84
+ ? `The list has ${names} already, saved${who}${when}. Read it first and send your save with the version it gives, so this one does not replace what is there.`
85
+ : `The list changed since this save was prepared: it has ${names} now, saved${who}${when}. Read it again and save with the version it gives; on the page, Save mine anyway keeps what is on screen.`;
86
+ }
87
+ // The entries a project's copy pronounces, under the project's own language and rules. An
88
+ // unparseable list is never fatal: its bad rows are skipped, and the editor is where they are
89
+ // named (the run's notices number the rows of the article's glossary, not of this list).
90
+ export function libraryGlossary(text, language, rules) {
91
+ if (text === undefined || text.trim() === "")
92
+ return { ok: true, entries: [] };
93
+ return parsePronunciationGlossary(text, language, rules);
94
+ }
@@ -4,7 +4,7 @@ import { stepFailedWhileWaiting } from "../admission/state-words.js";
4
4
  import { lastStarter } from "../agents/repo.js";
5
5
  import { projectHolds } from "../reviews/holds.js";
6
6
  import { isTemplateSample } from "../template-studio/repo.js";
7
- import { uploadedAndSettled } from "../uploads/repo.js";
7
+ import { settledProjects } from "../uploads/repo.js";
8
8
  import { settledStatus } from "../uploads/rules.js";
9
9
  import { atItsDay, failuresOf } from "./rules.js";
10
10
  // What a notification says about a project, read at the moment it goes out, by the rule the
@@ -14,7 +14,7 @@ export function noticeSubject(db, projectId) {
14
14
  if (project === undefined || isTemplateSample(db, projectId))
15
15
  return undefined;
16
16
  const stages = stagesOf(db, projectId);
17
- const settled = uploadedAndSettled(db, projectId);
17
+ const settled = settledProjects(db, projectId);
18
18
  const holds = projectHolds(db, { projectId, standings: new Map([[projectId, stages]]), settled });
19
19
  const status = settledStatus(derive(stages, project.paused), settled.has(projectId));
20
20
  const failures = failuresOf(stages);
@@ -12,7 +12,7 @@ import { projectDir } from "../storage/layout.js";
12
12
  // ceiling: one pass per layer of the step graph (text, narration, timing, picks, prompts,
13
13
  // images, renders); far more than any plan has.
14
14
  const rebindPasses = 40;
15
- export function copyProject(deps, sourceId, title) {
15
+ export function copyProject(deps, sourceId, title, { renamed = false } = {}) {
16
16
  const source = projectDir(deps.paths, sourceId);
17
17
  const tables = projectTables.map((table) => [table, projectRows(deps.db, table, sourceId)]);
18
18
  const ids = new Map([[sourceId, deps.ids.next()]]);
@@ -34,6 +34,9 @@ export function copyProject(deps, sourceId, title) {
34
34
  const projectId = ids.get(sourceId) ?? sourceId;
35
35
  const target = projectDir(deps.paths, projectId);
36
36
  const at = deps.clock.now().toISOString();
37
+ const named = (row) => renamed && typeof row.config === "string"
38
+ ? { ...row, config: renameConfig(row.config, title) }
39
+ : row;
37
40
  try {
38
41
  for (const file of listFiles(source)) {
39
42
  if (file === projectMarker)
@@ -52,7 +55,11 @@ export function copyProject(deps, sourceId, title) {
52
55
  column,
53
56
  typeof value === "string" ? remap(value) : value,
54
57
  ]));
55
- insert(deps.db, table, table === "projects" ? { ...copied, title, created_at: at, updated_at: at } : copied);
58
+ insert(deps.db, table, table === "projects"
59
+ ? { ...named(copied), title, created_at: at, updated_at: at }
60
+ : table === "project_revisions"
61
+ ? named(copied)
62
+ : copied);
56
63
  }
57
64
  rebind(deps, projectId);
58
65
  });
@@ -63,6 +70,16 @@ export function copyProject(deps, sourceId, title) {
63
70
  }
64
71
  return { projectId };
65
72
  }
73
+ // A stored config (JSON) under a new title, keeping what it was made about.
74
+ function renameConfig(config, title) {
75
+ const parsed = JSON.parse(config);
76
+ if (typeof parsed !== "object" || parsed === null || !("title" in parsed))
77
+ return config;
78
+ const before = parsed;
79
+ if (typeof before.title !== "string" || before.title === title)
80
+ return config;
81
+ return JSON.stringify({ ...before, title, subjectTitle: before.subjectTitle ?? before.title });
82
+ }
66
83
  // Re-stamps the copy's finished steps with the fingerprints its own ids give them.
67
84
  function rebind(deps, projectId) {
68
85
  const revisionId = currentRevisionId(deps.db, projectId);
@@ -1,14 +1,17 @@
1
- import { readinessIsUsable } from "../../kernel/ports/model.js";
2
1
  import { templateById } from "../project-templates/repo.js";
2
+ import { listVoices } from "../settings/repo.js";
3
3
  import { notTemplateSample } from "../template-studio/repo.js";
4
- import { starterPacks } from "./packs.js";
4
+ import { starterPacks, starterSet } from "./packs.js";
5
+ import { shortReadiness } from "./quick-short.js";
5
6
  import { sampleProjectIds } from "./sample.js";
6
7
  import { firstRunDismissed, readPackRecords } from "./state.js";
7
8
  // The first-run screen shows on a fresh install and never again once it is skipped or a real
8
9
  // project (anything but the samples and the template studio's hidden projects) exists. Reading it writes nothing: a real project that
9
10
  // exists before the screen was recorded as done is reported as `settle`, and the client
10
11
  // records it with POST /dismiss, so deleting every project later does not bring it back.
11
- export function firstRunView(db, statuses) {
12
+ export function firstRunView(db, statuses,
13
+ // How to install the computer's voice here, for the checklist's voice line.
14
+ install) {
12
15
  const samples = sampleProjectIds(db);
13
16
  const real = db
14
17
  .prepare(`SELECT 1 FROM projects WHERE id IS NOT ? AND id IS NOT ? AND id IS NOT ? AND ${notTemplateSample()} LIMIT 1`)
@@ -18,10 +21,15 @@ export function firstRunView(db, statuses) {
18
21
  return {
19
22
  show: !real && !dismissed,
20
23
  settle: real && !dismissed,
21
- voice: firstVoice(statuses),
24
+ // Every starter pack suggests the same voice, so the starter set's stands for all of them.
25
+ needs: shortReadiness({
26
+ statuses,
27
+ voices: listVoices(db),
28
+ suggested: starterSet.voice,
29
+ install,
30
+ }),
22
31
  sampleProjectId: samples.library,
23
32
  samples,
24
- clis: detectedClis(statuses),
25
33
  // A pack counts as added only with its template: the first short adds a pack's prompts and
26
34
  // voice but no template, and Add pack must still be able to add that template later (or
27
35
  // again, after it was deleted).
@@ -38,36 +46,3 @@ export function firstRunView(db, statuses) {
38
46
  }),
39
47
  };
40
48
  }
41
- // Who would narrate a first short: a keyed voice provider when one has a key, else the
42
- // computer's own voice when a speech program was found.
43
- export function firstVoice(statuses) {
44
- const keyed = statuses.find((row) => row.family === "tts" && row.readiness.kind === "keyed" && readinessIsUsable(row.readiness));
45
- const system = statuses.find((row) => row.readiness.kind === "local")?.readiness;
46
- const local = system?.kind === "local" ? system : undefined;
47
- return {
48
- keyed: keyed?.displayName ?? null,
49
- system: {
50
- available: local?.available === true,
51
- engine: local?.engine ?? null,
52
- issue: local?.issue ?? null,
53
- },
54
- };
55
- }
56
- // What this machine can make without a key: each agent CLI Slopify found, and whether it
57
- // answers (installed, a supported version, signed in as far as the probe can tell).
58
- export function detectedClis(statuses) {
59
- return ["claude-code", "codex", "gemini"].map((id) => {
60
- const status = statuses.find((row) => row.id === id);
61
- const readiness = status?.readiness;
62
- const cli = readiness?.kind === "cli" ? readiness : undefined;
63
- return {
64
- id,
65
- name: status?.displayName ?? id,
66
- installed: cli?.installed === true,
67
- ready: cli?.installed === true && cli.issue === undefined,
68
- version: cli?.version ?? null,
69
- issue: cli?.issue ?? null,
70
- draws: id === "codex",
71
- };
72
- });
73
- }
@@ -5,6 +5,7 @@ import { nameMax } from "../library/model.js";
5
5
  import { insertPrompt, promptById, promptByName } from "../library/repo.js";
6
6
  import { templateById, templateSummaries } from "../project-templates/repo.js";
7
7
  import { createTemplate } from "../project-templates/service.js";
8
+ import { readProviderDefaults } from "../settings/first-run.js";
8
9
  import { insertVoice, listVoices } from "../settings/repo.js";
9
10
  import { packById } from "./packs.js";
10
11
  import { readPackRecords, writePackRecord } from "./state.js";
@@ -46,7 +47,7 @@ export function installPack(deps, packId, options = { template: true }) {
46
47
  const created = createTemplate(deps, {
47
48
  id,
48
49
  name: freeTemplateName(deps, `${pack.name} starter`),
49
- document: packTemplate(pack, names, voice.voiceId),
50
+ document: packTemplate(pack, names, voice.voiceId, readProviderDefaults(deps.db)),
50
51
  });
51
52
  if (!created.ok)
52
53
  throw new Error(internalError(`the ${pack.name} pack's template was refused: ${created.reason}`));
@@ -10,6 +10,25 @@ export const sampleProjectsSchema = z.object({
10
10
  audiobook: z.string().nullable(),
11
11
  podcast: z.string().nullable(),
12
12
  });
13
+ // What a 60-second short needs that nothing on this computer does yet, with what fixes it. The
14
+ // first-run checklist and the refusal of "Make a 60-second short" say the same gaps.
15
+ export const shortGapSchema = z.object({
16
+ need: z.enum(["text", "voice", "images"]),
17
+ // What is missing and what fixes it, in a sentence or two.
18
+ message: z.string(),
19
+ // A key for that part, or (the voice) a voice saved for the voice service that has a key.
20
+ fix: z.enum(["key", "saved-voice"]),
21
+ // The voice only: how to install the computer's own free voice from a terminal, where that
22
+ // can be done.
23
+ install: z.object({ text: z.string(), commands: z.array(z.string()) }).optional(),
24
+ });
25
+ // Who does one part of a first short, by name ("Claude Code CLI", "OpenAI", "eSpeak NG"), and
26
+ // how: with the person's own sign-in to a tool on this computer, a saved key, or (the voice)
27
+ // the computer's own speech. Null while nothing can.
28
+ const shortPartSchema = z.object({
29
+ by: z.string().nullable(),
30
+ how: z.enum(["sign-in", "key", "computer"]).nullable(),
31
+ });
13
32
  export const firstRunViewSchema = z
14
33
  .object({
15
34
  show: z.boolean(),
@@ -17,31 +36,17 @@ export const firstRunViewSchema = z
17
36
  // with POST /dismiss, so deleting every project later does not bring the screen back.
18
37
  // Reading this view writes nothing.
19
38
  settle: z.boolean(),
20
- // Who narrates a first short: a voice provider with a saved key, else the computer's own.
21
- voice: z.object({
22
- // The keyed voice provider that would be used, by name ("OpenAI"), or null.
23
- keyed: z.string().nullable(),
24
- system: z.object({
25
- available: z.boolean(),
26
- // The speech program found ("eSpeak NG", "macOS voices").
27
- engine: z.string().nullable(),
28
- // Why none can be used, with the fix.
29
- issue: z.string().nullable(),
30
- }),
39
+ // The three things a short needs (the script, a voice and the images), each with who would
40
+ // do it, and the gaps, as "Make a 60-second short" would refuse them.
41
+ needs: z.object({
42
+ text: shortPartSchema,
43
+ voice: shortPartSchema,
44
+ images: shortPartSchema,
45
+ gaps: z.array(shortGapSchema),
31
46
  }),
32
47
  // The Library of Alexandria's project, as before the demos; `samples` has all three.
33
48
  sampleProjectId: z.string().nullable(),
34
49
  samples: sampleProjectsSchema,
35
- clis: z.array(z.object({
36
- id: z.enum(["claude-code", "codex", "gemini"]),
37
- name: z.string(),
38
- installed: z.boolean(),
39
- ready: z.boolean(),
40
- version: z.string().nullable(),
41
- issue: z.string().nullable(),
42
- // Codex draws the images too.
43
- draws: z.boolean(),
44
- })),
45
50
  packs: z.array(z.object({
46
51
  id: z.string(),
47
52
  name: z.string(),
@@ -0,0 +1,14 @@
1
+ // The model a service starts with when Slopify picks it for the person (a first short, the
2
+ // writer and voice Create fills in): a fast, capable default rather than the first listed,
3
+ // which can be the slowest or the smallest. One rule, so a service names the same model
4
+ // wherever it was picked for the person.
5
+ const preferred = {
6
+ "claude-code": /sonnet/i,
7
+ "openai-tts": /^gpt-4o-mini-tts$/,
8
+ };
9
+ export function isPreferredModel(provider, model) {
10
+ return preferred[provider]?.test(model) === true;
11
+ }
12
+ export function preferredModel(provider, models) {
13
+ return models.find((model) => isPreferredModel(provider, model.id)) ?? models[0];
14
+ }
@@ -2,18 +2,82 @@ import { readinessIsUsable } from "../../kernel/ports/model.js";
2
2
  import { titleMax } from "../admission/rules.js";
3
3
  import { defaultSentencePauseSeconds } from "../narration/pauses-model.js";
4
4
  import { systemVoiceProvider } from "../settings/model.js";
5
- const textClis = ["claude-code", "codex", "gemini"];
6
- const keyedImages = ["openai-image", "google-image", "fal", "replicate"];
5
+ import { preferredModel } from "./preferred-model.js";
6
+ // Who is asked first for each part: the CLIs before a key for the text, Codex before a key for
7
+ // the images.
8
+ const textOrder = ["claude-code", "codex", "gemini", "openrouter"];
9
+ const imageOrder = ["codex-image", "openai-image", "google-image", "fal", "replicate"];
7
10
  const keyedVoices = ["openai-tts", "elevenlabs", "cartesia", "inworld"];
8
- // The first model of these providers is the one a first short uses, unless one of these is
9
- // listed: a fast, capable default rather than the slowest or the smallest.
10
- const preferred = {
11
- "claude-code": /sonnet/i,
12
- "openai-tts": /^gpt-4o-mini-tts$/,
13
- };
14
11
  function ready(status) {
15
12
  return status !== undefined && readinessIsUsable(status.readiness);
16
13
  }
14
+ const statusOf = (statuses) => (id) => statuses.find((row) => row.id === id);
15
+ // The keyed voice a first short narrates with: a saved voice of a voice service with a key,
16
+ // else the voice the pack suggests when its service has a key.
17
+ function keyedVoice(statuses, voices, suggested) {
18
+ const status = statusOf(statuses);
19
+ const saved = voices.find((voice) => voice.provider !== systemVoiceProvider && ready(status(voice.provider)));
20
+ if (saved !== undefined)
21
+ return { provider: saved.provider, voiceId: saved.voiceId };
22
+ return ready(status(suggested.provider))
23
+ ? { provider: suggested.provider, voiceId: suggested.voiceId }
24
+ : undefined;
25
+ }
26
+ // One gap per part nothing can do, in plain words with what fixes it. The voice names a voice
27
+ // service that has a key but no saved voice, since a key alone is not the fix then.
28
+ function gapOf(need, statuses, install) {
29
+ if (need === "text")
30
+ return {
31
+ need,
32
+ fix: "key",
33
+ message: "Nothing can write the article yet. Sign in to Claude Code, Codex or Antigravity on this computer, or add an OpenRouter key.",
34
+ };
35
+ if (need === "images")
36
+ return {
37
+ need,
38
+ fix: "key",
39
+ message: "Nothing can make the images yet. Add an image key (OpenAI, Google, fal.ai or Replicate), or sign in to Codex on this computer.",
40
+ };
41
+ const status = statusOf(statuses);
42
+ const unsaved = keyedVoices.map(status).find((row) => ready(row));
43
+ if (unsaved !== undefined)
44
+ return {
45
+ need,
46
+ fix: "saved-voice",
47
+ message: `${unsaved.displayName} has a key but no voice saved yet. Save one in Settings → Voices, or add an OpenAI key, which works at once.`,
48
+ };
49
+ return {
50
+ need,
51
+ fix: "key",
52
+ message: "Nothing can read the article aloud yet. Add a voice key: an OpenAI key works at once with the voices Slopify sets up.",
53
+ ...(install === undefined ? {} : { install: { ...install, commands: [...install.commands] } }),
54
+ };
55
+ }
56
+ // What a first short would use on this computer and what it lacks, without asking any provider
57
+ // for its models: the first-run checklist, which must agree with the refusal below.
58
+ export function shortReadiness(input) {
59
+ const status = statusOf(input.statuses);
60
+ const part = (row) => row === undefined
61
+ ? { by: null, how: null }
62
+ : { by: row.displayName, how: row.readiness.kind === "cli" ? "sign-in" : "key" };
63
+ const text = part(textOrder.map(status).find((row) => ready(row)));
64
+ const images = part(imageOrder.map(status).find((row) => ready(row)));
65
+ const keyed = keyedVoice(input.statuses, input.voices, input.suggested);
66
+ const system = status(systemVoiceProvider)?.readiness;
67
+ const voice = keyed !== undefined
68
+ ? part(status(keyed.provider))
69
+ : system?.kind === "local" && system.available
70
+ ? { by: system.engine ?? "System voice", how: "computer" }
71
+ : { by: null, how: null };
72
+ const gaps = [
73
+ ["text", text],
74
+ ["images", images],
75
+ ["voice", voice],
76
+ ]
77
+ .filter(([, done]) => done.by === null)
78
+ .map(([need]) => gapOf(need, input.statuses, input.install));
79
+ return { text, voice, images, gaps };
80
+ }
17
81
  async function firstModel(modelsFor, provider, family) {
18
82
  let models;
19
83
  try {
@@ -22,58 +86,37 @@ async function firstModel(modelsFor, provider, family) {
22
86
  catch {
23
87
  return undefined;
24
88
  }
25
- const liked = preferred[provider];
26
- return ((liked === undefined ? undefined : models.find((model) => liked.test(model.id)))?.id ??
27
- models[0]?.id);
89
+ return preferredModel(provider, models)?.id;
28
90
  }
29
- export async function planShortProviders(input) {
30
- const status = (id) => input.statuses.find((row) => row.id === id);
31
- const gaps = [];
32
- let llm;
33
- for (const id of [...textClis, "openrouter"]) {
91
+ // The first ready provider in `order` that lists a model.
92
+ async function firstReady(input, order, family) {
93
+ const status = statusOf(input.statuses);
94
+ for (const id of order) {
34
95
  if (!ready(status(id)))
35
96
  continue;
36
- const model = await firstModel(input.modelsFor, id, "llm");
37
- if (model !== undefined) {
38
- llm = { provider: id, model };
39
- break;
40
- }
41
- }
42
- if (llm === undefined)
43
- gaps.push({
44
- need: "text",
45
- message: "No text model is ready. Install Claude Code, Codex or Antigravity CLI and sign in to it (Settings → Providers shows what Slopify found), or add an OpenRouter key in Settings → Providers.",
46
- });
47
- let images;
48
- for (const id of ["codex-image", ...keyedImages]) {
49
- if (!ready(status(id)))
50
- continue;
51
- const model = await firstModel(input.modelsFor, id, "image");
52
- if (model !== undefined) {
53
- images = { provider: id, model };
54
- break;
55
- }
97
+ const model = await firstModel(input.modelsFor, id, family);
98
+ if (model !== undefined)
99
+ return { provider: id, model };
56
100
  }
57
- if (images === undefined)
58
- gaps.push({
59
- need: "images",
60
- message: "No image model is ready. Install Codex CLI and sign in to it, or add an OpenAI, Google, fal.ai or Replicate key in Settings → Providers.",
61
- });
101
+ return undefined;
102
+ }
103
+ export async function planShortProviders(input) {
104
+ const status = statusOf(input.statuses);
105
+ const llm = await firstReady(input, textOrder, "llm");
106
+ const images = await firstReady(input, imageOrder, "image");
62
107
  // A keyed voice first (a saved one, else the pack's suggestion), then the computer's own: a
63
108
  // saved system voice, else the one found. Keys are the better voice, never a requirement.
64
109
  let audio;
65
- const keyed = (provider) => provider !== systemVoiceProvider;
66
- const saved = input.voices.find((voice) => keyed(voice.provider) && ready(status(voice.provider)));
67
- const provider = saved?.provider ??
68
- (ready(status(input.suggested.provider)) ? input.suggested.provider : undefined);
69
- if (provider !== undefined) {
70
- const model = await firstModel(input.modelsFor, provider, "tts");
110
+ const keyed = keyedVoice(input.statuses, input.voices, input.suggested);
111
+ if (keyed !== undefined) {
112
+ const model = await firstModel(input.modelsFor, keyed.provider, "tts");
71
113
  if (model !== undefined)
72
- audio = { provider, model, voice: saved?.voiceId ?? input.suggested.voiceId };
114
+ audio = { provider: keyed.provider, model, voice: keyed.voiceId };
73
115
  }
74
116
  let systemVoice = false;
75
- const system = status(systemVoiceProvider);
76
- if (audio === undefined && ready(system) && input.systemVoice !== undefined) {
117
+ if (audio === undefined &&
118
+ ready(status(systemVoiceProvider)) &&
119
+ input.systemVoice !== undefined) {
77
120
  const savedSystem = input.voices.find((voice) => voice.provider === systemVoiceProvider);
78
121
  audio = {
79
122
  provider: systemVoiceProvider,
@@ -82,20 +125,16 @@ export async function planShortProviders(input) {
82
125
  };
83
126
  systemVoice = true;
84
127
  }
85
- if (audio === undefined) {
86
- const systemIssue = system?.readiness.kind === "local" && system.readiness.issue !== undefined
87
- ? ` ${system.readiness.issue}`
88
- : "";
89
- gaps.push({
90
- need: "voice",
91
- message: keyedVoices.some((id) => ready(status(id)))
92
- ? "Your voice provider has no saved voice yet. Add one in Settings → Voices, then try again."
93
- : `No voice is ready to narrate the short: no voice key is saved and this computer's built-in voice can't be used.${systemIssue || " Install espeak-ng, or add an OpenAI, ElevenLabs, Cartesia or Inworld key in Settings → Providers (with ElevenLabs, Cartesia or Inworld, also save a voice in Settings → Voices)."}`,
94
- });
95
- }
96
- return llm === undefined || images === undefined || audio === undefined
97
- ? { ok: false, gaps }
98
- : { ok: true, providers: { llm, audio, images }, systemVoice };
128
+ if (llm === undefined || images === undefined || audio === undefined)
129
+ return {
130
+ ok: false,
131
+ gaps: [
132
+ ...(llm === undefined ? [gapOf("text", input.statuses, input.install)] : []),
133
+ ...(images === undefined ? [gapOf("images", input.statuses, input.install)] : []),
134
+ ...(audio === undefined ? [gapOf("voice", input.statuses, input.install)] : []),
135
+ ],
136
+ };
137
+ return { ok: true, providers: { llm, audio, images }, systemVoice };
99
138
  }
100
139
  // The short-mode draft for a topic, using the pack's (or starter set's) script and scene
101
140
  // prompts under the names they have in the library.
@@ -127,5 +127,8 @@ export function copySample(deps, projectId) {
127
127
  status: 404,
128
128
  detail: "That sample project is no longer here. Bring it back with Settings → Backup & storage → Restore samples, then press Make my own copy.",
129
129
  };
130
- return { ok: true, ...copyProject(deps, projectId, `${title} (my copy)`) };
130
+ return {
131
+ ok: true,
132
+ ...copyProject(deps, projectId, `${title} (my copy)`, { renamed: true }),
133
+ };
131
134
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readJsonSetting, readSetting, writeSetting } from "../settings/repo.js";
3
+ import { sampleIds } from "./model.js";
3
4
  // What the first five minutes remember, in the settings table: whether the first-run screen
4
5
  // was dismissed, which project is the bundled sample, and which library rows each installed
5
6
  // starter pack became. None of these keys is in a portable backup's settings, so another
@@ -41,6 +42,12 @@ const sampleKeyOf = (id) => (id === "library" ? sampleKey : `${sampleKey}.${id}`
41
42
  export function readSampleRecord(db, id = "library") {
42
43
  return readJson(db, sampleKeyOf(id), sampleRecord);
43
44
  }
45
+ // The bundled samples' projects, by id, for a slice that must leave them as they are (Disk
46
+ // space's clean-ups). A deleted sample's record stays until Restore samples, so an id may name
47
+ // no project any more.
48
+ export function bundledSampleProjects(db) {
49
+ return new Set(sampleIds.flatMap((id) => readSampleRecord(db, id)?.projectId ?? []));
50
+ }
44
51
  export function writeSampleRecord(db, record, id = "library") {
45
52
  writeSetting(db, sampleKeyOf(id), JSON.stringify(record));
46
53
  }
@@ -1,11 +1,12 @@
1
1
  import { defaultEdgeSilenceSeconds } from "../admission/rules.js";
2
2
  import { playDraftDocumentSchema } from "../play-drafts/schema.js";
3
3
  // A pack's Play template: its prompts ticked, its voice picked, its style set, and the text and
4
- // image providers left for the user to choose, since which of them is installed or keyed
5
- // differs per machine.
6
- export function packTemplate(pack, names, voiceId) {
4
+ // image providers this machine's first launch found (Claude Code, Codex), since which of them
5
+ // is installed or keyed differs per machine. One it did not find is left for the user to pick.
6
+ export function packTemplate(pack, names, voiceId, providers = {}) {
7
7
  const { style } = pack;
8
8
  const name = (key) => names[key] ?? "";
9
+ const chosen = (choice) => choice === undefined ? { provider: "", model: "" } : { ...choice };
9
10
  return playDraftDocumentSchema.parse({
10
11
  schemaVersion: 1,
11
12
  form: {
@@ -21,7 +22,7 @@ export function packTemplate(pack, names, voiceId) {
21
22
  video: "generate",
22
23
  document: "off",
23
24
  },
24
- llm: { provider: "", model: "" },
25
+ llm: chosen(providers.llm),
25
26
  audio: {
26
27
  provider: pack.voice.provider,
27
28
  model: pack.voice.model,
@@ -30,7 +31,7 @@ export function packTemplate(pack, names, voiceId) {
30
31
  shareGlossary: true,
31
32
  describeFigures: true,
32
33
  },
33
- images: { provider: "", model: "" },
34
+ images: chosen(providers.images),
34
35
  articlePrompt: name("article"),
35
36
  narrationPrompt: "",
36
37
  youtubeDescription: name("description") !== "",
@@ -1,4 +1,5 @@
1
1
  import { stageKinds } from "../../kernel/pipeline.js";
2
+ import { editSettings } from "../../kernel/ports/settings-places.js";
2
3
  import { sourceOf } from "../admission/model.js";
3
4
  import { defaultVoicesSettings } from "../voices/model.js";
4
5
  // Another output made from a project that already exists: the same brief, sources and
@@ -53,7 +54,7 @@ export function planAddedOutput(config, kind) {
53
54
  return {
54
55
  ok: false,
55
56
  reason: "narration-taken",
56
- message: `This project already has narration, and a project has one: ${kind === "podcast" ? "a podcast" : "an audiobook"} would replace it. Keep it, or change its format under Speakers in this project's Settings → Providers.`,
57
+ message: `This project already has narration, and a project has one: ${kind === "podcast" ? "a podcast" : "an audiobook"} would replace it. Keep it, or change its format under Speakers in ${editSettings.narration}.`,
57
58
  };
58
59
  const sources = switchedOn(config, kind);
59
60
  const next = {
@@ -78,7 +79,7 @@ export function planAddedOutput(config, kind) {
78
79
  reused: ["Brief: title, prompts and keywords", ...kept.map((stage) => stageResults[stage])],
79
80
  created: createdWords(kind, stages),
80
81
  textUse: kind === "podcast"
81
- ? "Adapted: the text model rewrites your article as a conversation between two hosts for the narration. The article stays as written; the conversation is kept beside it as the script."
82
+ ? "Adapted: the AI model rewrites your article as a conversation between two hosts for the narration. The article stays as written; the conversation is kept beside it as the script."
82
83
  : kind === "images"
83
84
  ? "Your text is not changed."
84
85
  : "Your text is used as written: nothing is rewritten.",
@@ -117,9 +117,9 @@ function newProblems(before, after) {
117
117
  .map((one) => ({ field: one.field, message: settingsHint(one) }));
118
118
  }
119
119
  function settingsHint(problem) {
120
- return /Settings|Edit project/.test(problem.message)
120
+ return /Settings|Edit settings/.test(problem.message)
121
121
  ? problem.message
122
- : `${problem.message} Set it in this project's Settings, save, then add the output again.`;
122
+ : `${problem.message} Set it in Edit settings, save, then add the output again.`;
123
123
  }
124
124
  function catalogueOf(deps) {
125
125
  return (deps.catalogue?.read() ?? {