@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
@@ -93,7 +93,7 @@ const refuse = (reason, message) => ({
93
93
  reason,
94
94
  message,
95
95
  });
96
- const missingTemplate = "This template no longer exists; it may have been deleted. Go to Library → Templates to see your templates.";
96
+ const missingTemplate = "This template no longer exists; it may have been deleted. Go to Templates to see your templates.";
97
97
  // What the bake-off is made on: the template's newest version, with a current paid sample.
98
98
  function basis(deps, templateId, section, models) {
99
99
  const template = templateById(deps.db, templateId);
@@ -106,7 +106,7 @@ function basis(deps, templateId, section, models) {
106
106
  const sample = sampleOf(deps.db, templateId);
107
107
  const made = sampleVersions(deps.db, templateId).find((one) => one.templateVersion === template.version);
108
108
  if (sample === undefined || !projectExists(deps.db, sample.projectId) || made?.state !== "done")
109
- return refuse("not-ready", `Compare models runs on the template's current sample, and version ${String(template.version)} has none yet. Press Save (or Make sample) in the studio, wait for it, then compare.`);
109
+ return refuse("not-ready", `Compare models runs on the template's current sample, and version ${String(template.version)} has none yet. Press Make sample, wait for it, then compare.`);
110
110
  const seen = new Set();
111
111
  for (const model of models) {
112
112
  const key = `${model.provider}:${model.model}`;
@@ -116,7 +116,7 @@ function basis(deps, templateId, section, models) {
116
116
  return refuse("invalid-input", `${model.model} is picked twice. Pick each model once.`);
117
117
  seen.add(key);
118
118
  if (section === "voice" && model.provider !== current.provider)
119
- return refuse("invalid-input", "A voice is chosen per provider, so voice models are compared within the template's voice provider. Pick models of that provider, or change the voice in Voice first.");
119
+ return refuse("invalid-input", "A voice belongs to its voice service, so voice models are compared within the template's voice service. Pick models of that service, or change the voice in Narration first.");
120
120
  }
121
121
  return { ok: true, document: template.document, version: template.version, current };
122
122
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { dailySampleBudget, dailySampleBudgetKey } from "../designs/budget.js";
3
3
  import { readJsonSetting, writeSetting } from "../settings/repo.js";
4
- // Settings → Template studio: the most the studio's samples may spend in one day, in dollars,
4
+ // Settings → Production defaults: the most the studio's samples may spend in one day, in dollars,
5
5
  // and whether they may use a CLI plan at all. Plan usage has no dollar price, so the budget
6
6
  // can't hold it back: samples that run on a CLI plan wait until the person turns that on. By
7
7
  // default ($2 a day, plans off) samples spend at most $2 a day and never a plan; at $0 nothing
@@ -91,13 +91,16 @@ export function fitsBudget(room, high, usesPlan = false) {
91
91
  return high <= room.left + 1e-9;
92
92
  }
93
93
  const money = (value) => `$${value.toFixed(2)}`;
94
+ // Settings → Production defaults' switch by its name on screen (web `budget-words.ts`
95
+ // plansLabel), so a sentence that says to turn it on names what the person will find.
96
+ export const plansSwitch = "Let samples use my Claude Code or Codex plan";
94
97
  // Why a sample that doesn't fit (`fitsBudget`) waits, and what lets it start, as one sentence
95
98
  // (no full stop) about `subject`: "This sample", "Comparing".
96
99
  export function budgetWords(room, cost, subject) {
97
100
  if (cost.usesPlan && !room.plans)
98
- return `${subject} runs on your CLI plan, and samples may not use CLI plans. Turn on Samples may use my CLI plans in Settings → Template studio`;
101
+ return `${subject} runs on your Claude Code or Codex plan, and samples may not use it. Turn on ${plansSwitch} in Settings → Production defaults`;
99
102
  const unpriced = room.unpriced === 0
100
103
  ? ""
101
104
  : ` (samples spent ${money(room.spent)} today plus ${String(room.unpriced)} ${room.unpriced === 1 ? "call" : "calls"} with no known price)`;
102
- return `${subject} costs about ${money(cost.high)} and ${money(room.left)} of today's ${money(room.budget)} sample budget is left${unpriced}. Raise the budget in Settings → Template studio`;
105
+ return `${subject} costs about ${money(cost.high)} and ${money(room.left)} of today's ${money(room.budget)} sample budget is left${unpriced}. Raise the budget in Settings → Production defaults`;
103
106
  }
@@ -0,0 +1,78 @@
1
+ import { projectExists } from "../admission/repo.js";
2
+ import { topicKeywords } from "../project-templates/one-off.js";
3
+ import { templateById, templateSummaries } from "../project-templates/repo.js";
4
+ import { currentRevisionId } from "../revisions/repo.js";
5
+ import { getRevisionView } from "../revisions/view.js";
6
+ import { listVoices } from "../settings/repo.js";
7
+ import { packSamples } from "./pack-samples.js";
8
+ import { sampleOf } from "./repo.js";
9
+ import { templateSchedules } from "./service.js";
10
+ const formats = new Set(["16:9", "9:16", "1:1"]);
11
+ export function cardFacts(document, voices) {
12
+ const { form } = document;
13
+ const voiced = form.sources.audio === "generate" && form.audio.voice.trim() !== "";
14
+ const saved = voices.find((one) => one.provider === form.audio.provider && one.voiceId === form.audio.voice);
15
+ const words = Number.parseInt(document.expectedWords, 10);
16
+ const shorts = Number.parseInt(form.shorts?.count ?? "0", 10);
17
+ return {
18
+ format: formats.has(form.format) ? form.format : "16:9",
19
+ video: form.sources.video !== "off" && form.sources.images !== "off",
20
+ words: form.sources.article === "off" || !Number.isFinite(words) ? null : Math.max(0, words),
21
+ voice: voiced
22
+ ? {
23
+ provider: form.audio.provider,
24
+ voiceId: form.audio.voice,
25
+ name: saved?.name.trim() ? saved.name : form.audio.voice,
26
+ }
27
+ : null,
28
+ captions: form.subtitles.mode,
29
+ shorts: form.shorts?.enabled === true && Number.isFinite(shorts) ? Math.max(0, shorts) : 0,
30
+ pdf: form.sources.document === "generate",
31
+ asks: [...topicKeywords(form.title)],
32
+ };
33
+ }
34
+ // The sample's thumbnail or first picture, as the project's file route serves it.
35
+ function sampleStill(deps, templateId) {
36
+ const sample = sampleOf(deps.db, templateId);
37
+ if (sample === undefined || !projectExists(deps.db, sample.projectId))
38
+ return null;
39
+ const revisionId = currentRevisionId(deps.db, sample.projectId);
40
+ if (revisionId === undefined)
41
+ return null;
42
+ const revision = getRevisionView({
43
+ db: deps.db,
44
+ paths: deps.paths,
45
+ log: deps.log,
46
+ ids: { next: () => "" },
47
+ clock: { now: () => new Date(), sleep: async () => undefined },
48
+ }, sample.projectId, revisionId);
49
+ const shown = (role) => revision?.outputs.find((row) => row.output.role === role && row.selected && row.available && row.state === "ready");
50
+ const row = shown("thumbnail") ?? shown("image");
51
+ return row === undefined
52
+ ? null
53
+ : `/files/${encodeURIComponent(sample.projectId)}/revisions/${encodeURIComponent(revisionId)}/${encodeURIComponent(row.recordId)}`;
54
+ }
55
+ function packStill(deps, templateId) {
56
+ const samples = packSamples(deps.paths, templateId);
57
+ const picture = samples.find((one) => one.role === "thumbnail") ??
58
+ samples.find((one) => one.role === "picture");
59
+ return picture === undefined
60
+ ? null
61
+ : `/api/template-studio/${templateId}/pack-samples/${picture.file}`;
62
+ }
63
+ export function templateCards(deps) {
64
+ const voices = listVoices(deps.db);
65
+ return templateSummaries(deps.db).flatMap((summary) => {
66
+ const template = templateById(deps.db, summary.id);
67
+ if (template === undefined)
68
+ return [];
69
+ return [
70
+ {
71
+ id: template.id,
72
+ still: sampleStill(deps, template.id) ?? packStill(deps, template.id),
73
+ ...cardFacts(template.document, voices),
74
+ schedules: templateSchedules(deps.db, template.id).length,
75
+ },
76
+ ];
77
+ });
78
+ }
@@ -1,6 +1,7 @@
1
1
  import { detectSlots, filledKeywords } from "../admission/substitute.js";
2
2
  import { isReserved } from "../keywords/model.js";
3
3
  import { topicKeywords } from "../project-templates/one-off.js";
4
+ import { withProviderDefaults, } from "../project-templates/provider-defaults.js";
4
5
  import { usesPostPrompt } from "../social/rules.js";
5
6
  import { sectionLabel } from "./model.js";
6
7
  export const freeCheckIds = [
@@ -16,13 +17,18 @@ export const studioHref = (templateId, section) => `/templates/${encodeURICompon
16
17
  const scheduleHref = (id) => `/calendar?tab=schedules&schedule=${encodeURIComponent(id)}`;
17
18
  const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
18
19
  const agentWindowMs = 7 * 24 * 60 * 60 * 1000;
20
+ // The checks in order. Providers is one line per missing thing, each with its own fix, so the
21
+ // count of lines to fix is the count of things to fix. A run that can't start can't be priced
22
+ // either: while a line above says why, the monthly cost's line would only say it again, so it
23
+ // waits until they are fixed.
19
24
  export function freeChecks(input) {
25
+ const start = [keywordsCheck(input), ...providersChecks(input), voiceCheck(input)];
26
+ const blocked = start.some((check) => !check.ok);
27
+ const cost = monthlyCostCheck(input);
20
28
  return [
21
- keywordsCheck(input),
22
- providersCheck(input),
23
- voiceCheck(input),
29
+ ...start,
24
30
  releaseSlotsCheck(input),
25
- monthlyCostCheck(input),
31
+ ...(blocked && !cost.ok ? [] : [cost]),
26
32
  answerersCheck(input),
27
33
  ];
28
34
  }
@@ -56,7 +62,7 @@ function keywordsCheck(input) {
56
62
  ok: false,
57
63
  sentence: `${empty.length === 1 ? "A keyword is" : "Keywords are"} empty: ${list(empty.map((name) => `{{${name}}}`))}. A run would send the prompt with nothing in ${empty.length === 1 ? "its place" : "their place"}.`,
58
64
  fix: {
59
- label: "Fill the keywords in Subject & research",
65
+ label: `Fill them in ${sectionLabel.subject}`,
60
66
  href: studioHref(input.templateId, "subject"),
61
67
  },
62
68
  };
@@ -79,8 +85,9 @@ function usedProviders(document) {
79
85
  {
80
86
  family: "llm",
81
87
  provider: form.llm.provider,
88
+ model: form.llm.model,
82
89
  section: "script",
83
- what: "text model",
90
+ what: "writing service",
84
91
  },
85
92
  ]
86
93
  : []),
@@ -89,8 +96,9 @@ function usedProviders(document) {
89
96
  {
90
97
  family: "tts",
91
98
  provider: form.audio.provider,
99
+ model: form.audio.model,
92
100
  section: "voice",
93
- what: "voice",
101
+ what: "voice service",
94
102
  },
95
103
  ]
96
104
  : []),
@@ -99,8 +107,9 @@ function usedProviders(document) {
99
107
  {
100
108
  family: "image",
101
109
  provider: form.images.provider,
110
+ model: form.images.model,
102
111
  section: "pictures",
103
- what: "picture model",
112
+ what: "image service",
104
113
  },
105
114
  ]
106
115
  : []),
@@ -109,43 +118,72 @@ function usedProviders(document) {
109
118
  {
110
119
  family: "llm",
111
120
  provider: form.reviews.provider,
121
+ model: form.reviews.model,
112
122
  section: "reviews",
113
- what: "reviewer model",
123
+ what: "service for the AI checks",
114
124
  },
115
125
  ]
116
126
  : []),
117
127
  ];
118
128
  }
119
- function providersCheck(input) {
120
- const used = usedProviders(input.document);
121
- const unchosen = used.find((one) => one.provider === "");
122
- if (unchosen !== undefined)
123
- return {
129
+ function providersChecks(input) {
130
+ const used = usedProviders(withProviderDefaults(input.document, input.defaults ?? {}));
131
+ const unchosen = used.filter((one, index) => one.provider === "" && used.findIndex((other) => other.what === one.what) === index);
132
+ // A service picked without its model is refused by every run, as the sample says.
133
+ const modelless = used.filter((one, index) => one.provider !== "" &&
134
+ one.model.trim() === "" &&
135
+ used.findIndex((other) => other.what === one.what) === index);
136
+ const missing = used.filter((one, index) => one.provider !== "" &&
137
+ input.providers.get(one.provider)?.ready !== true &&
138
+ used.findIndex((other) => other.provider === one.provider) === index);
139
+ const lines = [
140
+ ...unchosen.map((one) => ({
124
141
  id: "providers",
125
142
  ok: false,
126
- sentence: `No ${unchosen.what} is chosen, so the run can't start.`,
143
+ sentence: `No ${one.what} is chosen, so a run can't start.`,
127
144
  fix: {
128
- label: `Choose one in ${sectionLabel[unchosen.section]}`,
129
- href: studioHref(input.templateId, unchosen.section),
145
+ label: `Choose one in ${sectionLabel[one.section]}`,
146
+ href: studioHref(input.templateId, one.section),
130
147
  },
131
- };
132
- const missing = used.find((one) => input.providers.get(one.provider)?.ready !== true);
133
- if (missing === undefined)
134
- return { id: "providers", ok: true, sentence: "Every provider this template uses is set up." };
135
- const name = input.providers.get(missing.provider)?.name ?? missing.provider;
136
- return {
137
- id: "providers",
138
- ok: false,
139
- sentence: `${name} (the ${missing.what}) isn't set up on this computer: no key is stored or its program wasn't found.`,
140
- fix: { label: "Set it up in Settings → Providers", href: "/settings?section=providers" },
141
- };
148
+ })),
149
+ ...modelless.map((one) => {
150
+ const name = input.providers.get(one.provider)?.name ?? one.provider;
151
+ return {
152
+ id: "providers",
153
+ ok: false,
154
+ sentence: `No model is chosen for ${name}, the ${one.what}, so a run can't start.`,
155
+ fix: {
156
+ label: `Choose one in ${sectionLabel[one.section]}`,
157
+ href: studioHref(input.templateId, one.section),
158
+ },
159
+ };
160
+ }),
161
+ ...missing.map((one) => {
162
+ const name = input.providers.get(one.provider)?.name ?? one.provider;
163
+ return {
164
+ id: "providers",
165
+ ok: false,
166
+ sentence: `${name}, the ${one.what}, isn't set up on this computer: no key is stored or its program wasn't found.`,
167
+ fix: {
168
+ label: "Set it up in Settings → Keys & services",
169
+ href: "/settings?section=providers",
170
+ },
171
+ };
172
+ }),
173
+ ];
174
+ return lines.length > 0
175
+ ? lines
176
+ : [{ id: "providers", ok: true, sentence: "Every service this template uses is set up." }];
142
177
  }
143
178
  // The narration's voice (and each speaker's) is chosen and is one of the voices this computer
144
179
  // keeps in Settings → Voices: an imported pack's voice may be someone else's.
145
180
  function voiceCheck(input) {
146
181
  const { form } = input.document;
147
182
  const voiced = form.sources.audio === "generate";
148
- const fix = { label: "Choose a voice in Voice", href: studioHref(input.templateId, "voice") };
183
+ const fix = {
184
+ label: `Choose a voice in ${sectionLabel.voice}`,
185
+ href: studioHref(input.templateId, "voice"),
186
+ };
149
187
  if (!voiced)
150
188
  return { id: "voice", ok: true, sentence: "The narration isn't voiced by Slopify." };
151
189
  const picks = [
@@ -160,7 +198,7 @@ function voiceCheck(input) {
160
198
  return {
161
199
  id: "voice",
162
200
  ok: false,
163
- sentence: "No voice is chosen for the narration, so the run can't start.",
201
+ sentence: "No voice is chosen for the narration, so a run can't start.",
164
202
  fix,
165
203
  };
166
204
  const known = input.voices;
@@ -214,36 +252,47 @@ function releaseSlotsCheck(input) {
214
252
  : "Every run has a release time for the video and its shorts.",
215
253
  };
216
254
  }
255
+ // A run that can't be priced at all fails: it can't start either, and the sentence says why.
256
+ // Steps with no price (how many tables a written article describes is known only once it is
257
+ // written) never fail it, since nothing fixes them: they are named as not counted, here and in
258
+ // Schedule, so the dollar figure reads as the least it costs.
217
259
  function monthlyCostCheck(input) {
218
- if (input.monthlyCost === null)
260
+ const cost = input.monthlyCost;
261
+ if (cost === null)
219
262
  return {
220
263
  id: "monthly-cost",
221
264
  ok: true,
222
265
  sentence: "No schedule runs this template, so it costs nothing a month.",
223
266
  };
224
- const paused = input.monthlyCost.paused?.schedules ?? 0;
225
- const resumed = `up to about $${(input.monthlyCost.paused?.high ?? 0).toFixed(2)} a month when resumed`;
226
- const many = (n) => `${String(n)} paused ${n === 1 ? "schedule" : "schedules"}`;
227
- if (input.monthlyCost.unknown === 0) {
228
- const activeCost = `Its schedules cost up to about $${input.monthlyCost.high.toFixed(2)} a month`;
267
+ if (cost.problem !== undefined && cost.problem !== null)
229
268
  return {
230
269
  id: "monthly-cost",
231
- ok: true,
232
- sentence: paused === 0
233
- ? `${activeCost}.`
234
- : input.monthlyCost.high === 0 && (input.monthlyCost.runs ?? 0) === 0
235
- ? `${many(paused)} ${paused === 1 ? "uses" : "use"} this template: ${paused === 1 ? "it costs" : "they cost"} nothing while paused, and ${resumed}.`
236
- : `${activeCost}; ${many(paused)} would add ${resumed}.`,
270
+ ok: false,
271
+ sentence: cost.problem,
272
+ fix: { label: "See its runs in Schedule", href: studioHref(input.templateId, "schedule") },
237
273
  };
238
- }
274
+ const paused = cost.paused?.schedules ?? 0;
275
+ const resumed = `up to about $${(cost.paused?.high ?? 0).toFixed(2)} a month when resumed`;
276
+ const many = (n) => `${String(n)} paused ${n === 1 ? "schedule" : "schedules"}`;
277
+ const activeCost = `Its schedules cost ${cost.unknown === 0 ? "up to about" : "at least"} $${cost.high.toFixed(2)} a month`;
278
+ const sentence = paused === 0
279
+ ? `${activeCost}.`
280
+ : cost.high === 0 && (cost.runs ?? 0) === 0
281
+ ? `${many(paused)} ${paused === 1 ? "uses" : "use"} this template: ${paused === 1 ? "it costs" : "they cost"} nothing while paused, and ${resumed}.`
282
+ : `${activeCost}; ${many(paused)} would add ${resumed}.`;
239
283
  return {
240
284
  id: "monthly-cost",
241
- ok: false,
242
- sentence: input.monthlyCost.problem ??
243
- `The monthly cost isn't known: ${String(input.monthlyCost.unknown)} ${input.monthlyCost.unknown === 1 ? "step has" : "steps have"} no price in the model list.`,
244
- fix: { label: "See which in Schedule", href: studioHref(input.templateId, "schedule") },
285
+ ok: true,
286
+ sentence: cost.unknown === 0 ? sentence : `${sentence} ${unpricedSentence(cost)}`,
245
287
  };
246
288
  }
289
+ // "Not counted, with no known price: Narration descriptions and Delivery cue overhead."
290
+ function unpricedSentence(cost) {
291
+ const names = cost.unpriced ?? [];
292
+ return names.length === 0
293
+ ? `${String(cost.unknown)} ${cost.unknown === 1 ? "step has" : "steps have"} no known price and ${cost.unknown === 1 ? "isn't" : "aren't"} counted.`
294
+ : `Not counted, with no known price: ${list(names)}.`;
295
+ }
247
296
  function answerersCheck(input) {
248
297
  const { form } = input.document;
249
298
  const asks = (form.checkpoints?.length ?? 0) > 0 ||
@@ -258,13 +307,13 @@ function answerersCheck(input) {
258
307
  id: "answerers",
259
308
  ok: true,
260
309
  sentence: agent
261
- ? "An agent connected this week answers the checkpoints."
262
- : "You're notified when a checkpoint waits.",
310
+ ? "An agent connected this week answers when a run pauses."
311
+ : "You're notified when a run pauses for you.",
263
312
  };
264
313
  return {
265
314
  id: "answerers",
266
315
  ok: false,
267
- sentence: "A run stops at a checkpoint and nobody would know: no notification address is set and no agent connected in the last 7 days.",
316
+ sentence: "A run pauses for your approval and nobody would know: no notification address is set and no agent connected in the last 7 days.",
268
317
  fix: {
269
318
  label: "Set a notification address in Settings → Notifications",
270
319
  href: "/settings?section=notifications",
@@ -2,7 +2,7 @@ import { setImmediate as yieldTurn } from "node:timers/promises";
2
2
  import { z } from "zod";
3
3
  import { transact } from "../../kernel/db/tx.js";
4
4
  import { templateById, templateSummaries } from "../project-templates/repo.js";
5
- import { budgetRoom, budgetWords, fitsBudget } from "./budget.js";
5
+ import { budgetRoom, budgetWords, fitsBudget, plansSwitch } from "./budget.js";
6
6
  import { enqueueJob } from "./jobs.js";
7
7
  import { quoteSample } from "./quote.js";
8
8
  import { sampleVersions, setSampleVersion } from "./repo.js";
@@ -79,10 +79,10 @@ function sentenceOf(makeable, room) {
79
79
  const waiting = count - fitting;
80
80
  const onPlan = makeable.filter((item) => !item.fits && item.usesPlan && !room.plans).length;
81
81
  const why = onPlan === waiting
82
- ? ": they run on your CLI plan, and samples may not use CLI plans (turn on Samples may use my CLI plans in Settings → Template studio)"
82
+ ? `: they run on your Claude Code or Codex plan, which samples may not use (turn on ${plansSwitch} in Settings → Production defaults)`
83
83
  : onPlan > 0
84
- ? `: ${String(onPlan)} run on your CLI plan, which samples may not use (Samples may use my CLI plans in Settings → Template studio), and the rest wait for tomorrow or a higher budget`
85
- : " for tomorrow or a higher budget in Settings → Template studio";
84
+ ? `: ${String(onPlan)} run on your Claude Code or Codex plan, which samples may not use (turn on ${plansSwitch} in Settings → Production defaults), and the rest wait for tomorrow or a higher budget`
85
+ : " for tomorrow or a higher budget in Settings → Production defaults";
86
86
  return `${head} ${String(fitting)} of them ${fitting === 1 ? "fits" : "fit"} today's budget; the other ${String(waiting)} wait${why}.`;
87
87
  }
88
88
  export const samplesForAllSchema = z
@@ -94,7 +94,7 @@ export const samplesForAllSchema = z
94
94
  })
95
95
  .strict()
96
96
  .readonly();
97
- const budgetHeld = "Waiting: today's sample budget is used by the other templates' samples. Press Make sample tomorrow, or raise the budget in Settings → Template studio.";
97
+ const budgetHeld = "Waiting: today's sample budget is used by the other templates' samples. Press Make sample tomorrow, or raise the budget in Settings → Production defaults.";
98
98
  export async function makeSamplesForAll(deps, raw) {
99
99
  const parsed = samplesForAllSchema.safeParse(raw);
100
100
  if (!parsed.success)
@@ -1,5 +1,5 @@
1
1
  import { defaultImageSeconds, defaultMotionStyle, defaultZoomPercent } from "../admission/rules.js";
2
- // The Video & captions sample: 30 seconds made by the style preview's renderer from the
2
+ // The Video and style sample: 30 seconds made by the style preview's renderer from the
3
3
  // template's frame, captions, Look and picture timing, on the bundled sample's pictures and
4
4
  // narration. Local and free; no provider is called.
5
5
  export const videoSampleSeconds = 30;
@@ -8,7 +8,7 @@ import { loudnessGoalOf, loudnessHash } from "./loudness.js";
8
8
  // and after, and the PDF. Each is kept by the hash of what it is made from, so a setting changed
9
9
  // back, or the same setup in another template, shows at once.
10
10
  export const freeKinds = ["video", "loudness", "document"];
11
- const press = "press Make samples";
11
+ const press = "press Make sample";
12
12
  export function createFreeSamples(deps) {
13
13
  const failures = new Map();
14
14
  const making = new Set();
@@ -71,7 +71,7 @@ export function createFreeSamples(deps) {
71
71
  if (form.sources.document === "generate" && deps.documents !== undefined) {
72
72
  const documents = deps.documents;
73
73
  const theme = documentThemeOfTemplate(document);
74
- await attempt("document", documentHash(theme), () => documents.make(theme), (error) => `The PDF sample couldn't be made: ${errorText(error)} Change the theme in Document (PDF) → Fine-tune, then ${press}.`);
74
+ await attempt("document", documentHash(theme), () => documents.make(theme), (error) => `The PDF sample couldn't be made: ${errorText(error)} Change the theme under All PDF settings, then ${press}.`);
75
75
  }
76
76
  },
77
77
  view: (templateId, document, job) => {
@@ -129,7 +129,7 @@ export function createFreeSamples(deps) {
129
129
  loudness = {
130
130
  ...(shown ?? {
131
131
  state: "current",
132
- sentence: "Slopify's sample narration as it came, and levelled the way your videos will be. Your own voice's sample replaces it once paid samples are made.",
132
+ sentence: "How your narration will be levelled, shown on Slopify's sample narration until your voice's own sample is made.",
133
133
  url: `${base}/loudness/after.mp3?v=${lHash.slice(0, 12)}`,
134
134
  }),
135
135
  beforeUrl: `${base}/loudness/before.mp3`,
@@ -141,7 +141,7 @@ export function createFreeSamples(deps) {
141
141
  }
142
142
  let pdf;
143
143
  if (form.sources.document !== "generate")
144
- pdf = { state: "off", sentence: "The PDF document is off.", url: null };
144
+ pdf = { state: "off", sentence: "The PDF is off.", url: null };
145
145
  else if (deps.documents === undefined)
146
146
  pdf = unavailable("PDF sample");
147
147
  else {
@@ -162,7 +162,7 @@ export function createFreeSamples(deps) {
162
162
  function videoFailure(error) {
163
163
  if (error instanceof StylePreviewError) {
164
164
  if (error.reason === "font-missing")
165
- return `The video sample couldn't be made because the caption font is no longer on this computer. Choose another font in Video & captions → Fine-tune, then ${press}.`;
165
+ return `The video sample couldn't be made because the caption font is no longer on this computer. Choose another Caption font in Video and style, then ${press}.`;
166
166
  if (error.reason === "ffmpeg-missing")
167
167
  return `The video sample couldn't be made because ffmpeg wasn't found. Install ffmpeg, or use the Docker image, then ${press}.`;
168
168
  }
@@ -1,6 +1,7 @@
1
- // Template studio (4.0): a template is set up once, section by section, with live samples on a
2
- // hidden project, then marked ready for its schedules. Not to be confused with `slices/studio`,
3
- // which is the YouTube Studio pack and its extension.
1
+ import { setupSectionNames } from "../project-templates/setup-sections.js";
2
+ // Template studio (4.0, "Edit template" on screen): a template is set up once, section by
3
+ // section, with live samples on a hidden project, then approved for its schedules. Not to be
4
+ // confused with `slices/studio`, which is the YouTube Studio pack and its extension.
4
5
  // In pipeline order: the studio's rail shows them so.
5
6
  export const templateSections = [
6
7
  "subject",
@@ -15,19 +16,22 @@ export const templateSections = [
15
16
  "upload",
16
17
  "schedule",
17
18
  ];
18
- export const sectionLabel = {
19
- subject: "Subject & research",
20
- script: "Script",
21
- voice: "Voice",
22
- pictures: "Pictures",
23
- video: "Video & captions",
24
- thumbnail: "Thumbnail & title",
25
- shorts: "Shorts",
26
- document: "Document (PDF)",
27
- reviews: "Reviews & checkpoints",
28
- upload: "Upload pack",
29
- schedule: "Schedule",
19
+ // Each section's place in the one list of setup sections Create, Edit template and a project's
20
+ // settings share. The ids stay as they were, so every saved address still opens its section.
21
+ export const setupSectionOf = {
22
+ subject: "topic",
23
+ script: "article",
24
+ voice: "narration",
25
+ pictures: "images",
26
+ video: "video",
27
+ thumbnail: "thumbnail",
28
+ shorts: "shorts",
29
+ document: "pdf",
30
+ reviews: "checks",
31
+ upload: "youtube",
32
+ schedule: "schedule",
30
33
  };
34
+ export const sectionLabel = Object.fromEntries(templateSections.map((id) => [id, setupSectionNames[setupSectionOf[id]]]));
31
35
  // What a section's sample is: made for the current version, being made, queued behind another,
32
36
  // failed with a fix, made for an older version, switched off, or provided by the user at run time.
33
37
  export const sectionStates = [
@@ -15,14 +15,14 @@ const refuse = (message) => ({
15
15
  reason: "invalid-input",
16
16
  message,
17
17
  });
18
- const notAPack = "This file isn't a template pack Slopify can read. Choose the .zip that Export pack in a template's studio made.";
18
+ const notAPack = "This file isn't a template file Slopify can read. Choose the .zip a template's Export as a file (.zip) made.";
19
19
  class ZipStopped extends Error {
20
20
  }
21
21
  // Every check that needs nothing written: a zip, small enough, with a manifest of this format
22
22
  // and version, no keys, and every sample it names inside it.
23
23
  export function readPack(bytes) {
24
24
  if (bytes.byteLength > packLimits.uploadBytes)
25
- return refuse(`This pack is larger than ${String(packLimits.uploadBytes / 1024 / 1024)} MB, more than a template pack ever is. Export it again from the template's studio, or ask whoever made it to.`);
25
+ return refuse(`This template file is larger than ${String(packLimits.uploadBytes / 1024 / 1024)} MB, more than an exported template ever is. Export it again from the template's Edit template page, or ask whoever made it to.`);
26
26
  let oversized;
27
27
  let unexpected;
28
28
  let twice;
@@ -73,42 +73,42 @@ export function readPack(bytes) {
73
73
  entries = {};
74
74
  }
75
75
  if (tooMany)
76
- return refuse(`This pack holds more files than a template pack ever has (a manifest and at most ${String(packLimits.samples)} samples), so nothing was imported. Export the pack again from the template's studio.`);
76
+ return refuse(`This template file holds more files than an exported template ever has (a manifest and at most ${String(packLimits.samples)} samples), so nothing was imported. Export it again from the template's Edit template page.`);
77
77
  if (twice !== undefined)
78
- return refuse(`This pack holds ${twice.slice(0, 120)} twice, which an exported pack never does, so nothing was imported. Export the pack again from the template's studio.`);
78
+ return refuse(`This template file holds ${twice.slice(0, 120)} twice, which an exported template never does, so nothing was imported. Export it again from the template's Edit template page.`);
79
79
  if (oversized !== undefined)
80
- return refuse(`This pack's ${oversized} is larger than a template pack allows, so nothing was imported. Export the pack again from the template's studio.`);
80
+ return refuse(`This template file's ${oversized} is larger than a template file allows, so nothing was imported. Export it again from the template's Edit template page.`);
81
81
  if (unexpected !== undefined)
82
- return refuse(`This pack holds a file a template pack never has (${unexpected.slice(0, 120)}), so nothing was imported. Export the pack again from the template's studio.`);
82
+ return refuse(`This template file holds a file an exported template never has (${unexpected.slice(0, 120)}), so nothing was imported. Export it again from the template's Edit template page.`);
83
83
  const raw = entries["manifest.json"];
84
84
  if (raw === undefined)
85
- return refuse("This zip has no manifest.json, so it isn't a template pack. Choose the .zip that Export pack in a template's studio made.");
85
+ return refuse("This zip has no manifest.json, so it isn't a template file. Choose the .zip a template's Export as a file (.zip) made.");
86
86
  const text = strFromU8(raw);
87
87
  if (looksSecret(text))
88
- return refuse("This pack holds what looks like an API key or a login token, so Slopify didn't import it: keys never travel in a pack. Ask whoever made it to remove it from the template and export it again.");
88
+ return refuse("This template file holds what looks like an API key or a login token, so Slopify didn't import it: keys never travel in a template file. Ask whoever made it to remove it from the template and export it again.");
89
89
  let json;
90
90
  try {
91
91
  json = JSON.parse(text);
92
92
  }
93
93
  catch {
94
- return refuse("This pack's manifest.json isn't readable JSON; the file may be damaged. Export the pack again from the template's studio.");
94
+ return refuse("This template file's manifest.json isn't readable JSON; the file may be damaged. Export it again from the template's Edit template page.");
95
95
  }
96
96
  const head = json;
97
97
  if (typeof json !== "object" || json === null || head.format !== packFormat)
98
98
  return refuse(notAPack);
99
99
  if (typeof head.formatVersion === "number" && head.formatVersion > packFormatVersion)
100
- return refuse(`This pack was made by a newer Slopify (pack format ${String(head.formatVersion)}). Update Slopify in Settings → About, then import it again.`);
100
+ return refuse(`This template file was made by a newer Slopify (file format ${String(head.formatVersion)}). Update Slopify in Settings → About, then import it again.`);
101
101
  const parsed = packManifestSchema.safeParse(json);
102
102
  if (!parsed.success) {
103
103
  const issue = parsed.error.issues[0];
104
104
  const where = issue === undefined ? "" : ` (${issue.path.join(".") || "manifest"}: ${issue.message})`;
105
- return refuse(`This pack's manifest isn't one Slopify can use${where.slice(0, 300)}. It may be damaged or edited by hand; export it again from the template's studio.`);
105
+ return refuse(`This template file's manifest isn't one Slopify can use${where.slice(0, 300)}. It may be damaged or edited by hand; export it again from the template's Edit template page.`);
106
106
  }
107
107
  const files = new Map();
108
108
  for (const sample of parsed.data.samples) {
109
109
  const file = entries[sample.file];
110
110
  if (file === undefined)
111
- return refuse(`This pack names a sample (${sample.file}) it doesn't hold; the file may be damaged. Export the pack again from the template's studio.`);
111
+ return refuse(`This template file names a sample (${sample.file}) it doesn't hold; the file may be damaged. Export it again from the template's Edit template page.`);
112
112
  files.set(sample.file, file);
113
113
  }
114
114
  return { ok: true, value: { manifest: parsed.data, files } };
@@ -198,8 +198,8 @@ export function importPack(deps, bytes) {
198
198
  });
199
199
  if (!created.ok)
200
200
  throw new PackRefused(created.reason === "missing-prompt"
201
- ? "This pack's template names a prompt or intro/outro the pack doesn't carry, so it can't be made here. Ask whoever made it to export it again."
202
- : "Slopify couldn't make a template from this pack. Import it again; if it fails again, use Download diagnostics in Settings and report it.");
201
+ ? "This file's template names a prompt or intro/outro the file doesn't carry, so it can't be made here. Ask whoever made it to export it again."
202
+ : "Slopify couldn't make a template from this file. Import it again; if it fails again, use Download diagnostics in Settings and report it.");
203
203
  const audio = created.value.document.form.audio;
204
204
  const voiceKnown = audio.voice === "" ||
205
205
  listVoices(deps.db).some((voice) => voice.provider === audio.provider && voice.voiceId === audio.voice);