@gentbajko/slopify 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/dist/adapters/account-limits.js +1 -1
  2. package/dist/adapters/alignment/audio.js +1 -1
  3. package/dist/adapters/alignment/cache.js +1 -1
  4. package/dist/adapters/alignment/multilingual.js +2 -1
  5. package/dist/adapters/alignment/runner.js +1 -1
  6. package/dist/adapters/alignment/worker.js +1 -1
  7. package/dist/adapters/explain.js +10 -9
  8. package/dist/adapters/host-cli/index.js +2 -1
  9. package/dist/adapters/host-cli/transport.js +2 -1
  10. package/dist/adapters/image/bytes.js +2 -1
  11. package/dist/adapters/image/codex.js +6 -5
  12. package/dist/adapters/image/fal.js +4 -3
  13. package/dist/adapters/image/google.js +3 -2
  14. package/dist/adapters/image/models.js +2 -2
  15. package/dist/adapters/image/openai.js +2 -1
  16. package/dist/adapters/image/replicate.js +3 -2
  17. package/dist/adapters/image/video.js +2 -1
  18. package/dist/adapters/key-probes.js +1 -1
  19. package/dist/adapters/llm/antigravity-models.js +1 -1
  20. package/dist/adapters/llm/antigravity.js +7 -6
  21. package/dist/adapters/llm/assistant-cli.js +1 -1
  22. package/dist/adapters/llm/claude-code-models.js +1 -1
  23. package/dist/adapters/llm/claude-code.js +3 -2
  24. package/dist/adapters/llm/codex-models.js +1 -1
  25. package/dist/adapters/llm/document-workspace.js +3 -2
  26. package/dist/adapters/llm/image-workspace.js +3 -2
  27. package/dist/adapters/llm/openrouter.js +2 -1
  28. package/dist/adapters/llm/run-cli.js +3 -2
  29. package/dist/adapters/tts/gemini.js +2 -1
  30. package/dist/adapters/tts/inworld-async.js +2 -1
  31. package/dist/adapters/tts/inworld.js +3 -2
  32. package/dist/adapters/tts/system.js +2 -1
  33. package/dist/catalog/registry.js +20 -13
  34. package/dist/catalog/schema.js +2 -1
  35. package/dist/catalog/validate.js +5 -2
  36. package/dist/edge/http/actions.js +7 -7
  37. package/dist/edge/http/app.js +3 -3
  38. package/dist/edge/http/assistant.js +3 -1
  39. package/dist/edge/http/backups.js +1 -1
  40. package/dist/edge/http/channel-memory.js +2 -2
  41. package/dist/edge/http/channels.js +1 -1
  42. package/dist/edge/http/checkpoints.js +7 -5
  43. package/dist/edge/http/design-templates.js +3 -3
  44. package/dist/edge/http/designs.js +1 -1
  45. package/dist/edge/http/document-themes.js +3 -3
  46. package/dist/edge/http/draft-problem.js +1 -1
  47. package/dist/edge/http/extension-auth.js +1 -1
  48. package/dist/edge/http/home.js +9 -1
  49. package/dist/edge/http/model-checks.js +4 -4
  50. package/dist/edge/http/onboarding.js +29 -12
  51. package/dist/edge/http/outputs.js +3 -2
  52. package/dist/edge/http/patch-notes.js +2 -2
  53. package/dist/edge/http/planning.js +1 -1
  54. package/dist/edge/http/project-create.js +1 -1
  55. package/dist/edge/http/project-templates.js +2 -2
  56. package/dist/edge/http/projects.js +22 -4
  57. package/dist/edge/http/prompts.js +2 -2
  58. package/dist/edge/http/providers.js +9 -10
  59. package/dist/edge/http/refusal.js +1 -1
  60. package/dist/edge/http/remote-access.js +4 -4
  61. package/dist/edge/http/remote-guard.js +2 -2
  62. package/dist/edge/http/reviews.js +1 -1
  63. package/dist/edge/http/revision-files.js +1 -1
  64. package/dist/edge/http/revisions.js +4 -4
  65. package/dist/edge/http/schedules.js +13 -11
  66. package/dist/edge/http/settings.js +6 -2
  67. package/dist/edge/http/social.js +5 -4
  68. package/dist/edge/http/staging.js +1 -1
  69. package/dist/edge/http/storage.js +12 -4
  70. package/dist/edge/http/studio.js +71 -19
  71. package/dist/edge/http/style-preview.js +2 -2
  72. package/dist/edge/http/template-studio.js +12 -9
  73. package/dist/edge/http/trash.js +1 -1
  74. package/dist/edge/http/youtube-edits.js +1 -1
  75. package/dist/edge/mcp/excluded-routes.js +4 -3
  76. package/dist/edge/mcp/instructions.js +3 -3
  77. package/dist/edge/mcp/library-tools.js +8 -5
  78. package/dist/edge/mcp/remake-tools.js +7 -4
  79. package/dist/edge/mcp/server.js +1 -1
  80. package/dist/edge/mcp/tools-channels.js +9 -9
  81. package/dist/edge/mcp/tools-designs.js +9 -9
  82. package/dist/edge/mcp/tools-library.js +7 -7
  83. package/dist/edge/mcp/tools-planning.js +18 -18
  84. package/dist/edge/mcp/tools-projects.js +10 -10
  85. package/dist/edge/mcp/tools-publishing.js +9 -9
  86. package/dist/edge/mcp/tools-runs.js +9 -9
  87. package/dist/edge/mcp/tools-schedules.js +6 -6
  88. package/dist/edge/mcp/tools-setup.js +20 -14
  89. package/dist/edge/mcp/tools-social.js +5 -4
  90. package/dist/edge/mcp/tools-storage.js +3 -3
  91. package/dist/edge/mcp/tools-template-studio.js +18 -18
  92. package/dist/edge/remote-access/model.js +1 -1
  93. package/dist/extension/slopify-studio-chrome.zip +0 -0
  94. package/dist/extension/slopify-studio-firefox.zip +0 -0
  95. package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
  96. package/dist/kernel/pipeline.js +12 -0
  97. package/dist/kernel/ports/settings-places.js +31 -0
  98. package/dist/kernel/ports/system-speech.js +31 -3
  99. package/dist/kernel/runner/attempt.js +11 -3
  100. package/dist/kernel/runner/providers.js +3 -2
  101. package/dist/kernel/runner/standalone.js +2 -2
  102. package/dist/main.js +5 -3
  103. package/dist/model-catalog.js +20 -0
  104. package/dist/patch-notes/4.0.0.md +13 -13
  105. package/dist/patch-notes/4.1.0.md +42 -0
  106. package/dist/patch-notes/index.json +6 -0
  107. package/dist/slices/admission/listing-facts.js +95 -0
  108. package/dist/slices/admission/repo.js +9 -6
  109. package/dist/slices/admission/rules.js +7 -7
  110. package/dist/slices/admission/state-words.js +34 -5
  111. package/dist/slices/assistant/cost.js +1 -1
  112. package/dist/slices/assistant/model.js +10 -3
  113. package/dist/slices/assistant/turn.js +5 -3
  114. package/dist/slices/autopilot/warnings.js +1 -1
  115. package/dist/slices/backups/service.js +5 -5
  116. package/dist/slices/channels/cast-images.js +1 -1
  117. package/dist/slices/channels/schema.js +1 -1
  118. package/dist/slices/channels/videos.js +1 -1
  119. package/dist/slices/control/providers.js +2 -2
  120. package/dist/slices/designs/budget.js +1 -1
  121. package/dist/slices/designs/calls.js +2 -2
  122. package/dist/slices/designs/check-context.js +4 -4
  123. package/dist/slices/designs/fields.js +4 -4
  124. package/dist/slices/designs/gate.js +7 -5
  125. package/dist/slices/designs/render-input.js +1 -1
  126. package/dist/slices/designs/requests.js +1 -1
  127. package/dist/slices/designs/runner.js +1 -1
  128. package/dist/slices/designs/sample.js +2 -1
  129. package/dist/slices/designs/service.js +1 -1
  130. package/dist/slices/designs/template-checks.js +2 -2
  131. package/dist/slices/designs/template-make.js +3 -3
  132. package/dist/slices/designs/templates.js +3 -3
  133. package/dist/slices/document/fonts.js +1 -1
  134. package/dist/slices/document/model.js +7 -4
  135. package/dist/slices/document/render.js +2 -1
  136. package/dist/slices/document/theme-schema.js +1 -1
  137. package/dist/slices/document/theme.js +49 -3
  138. package/dist/slices/episodes/summarize.js +2 -2
  139. package/dist/slices/estimate/index.js +28 -22
  140. package/dist/slices/estimate/requests.js +4 -4
  141. package/dist/slices/fixes/rules.js +3 -1
  142. package/dist/slices/fonts/catalog.js +2 -1
  143. package/dist/slices/fonts/coverage.js +2 -1
  144. package/dist/slices/images/appearance.js +2 -1
  145. package/dist/slices/images/scenes.js +2 -1
  146. package/dist/slices/keywords/digest.js +2 -1
  147. package/dist/slices/keywords/rules.js +2 -2
  148. package/dist/slices/keywords/specs.js +5 -5
  149. package/dist/slices/keywords/values.js +1 -1
  150. package/dist/slices/library/transfer.js +3 -3
  151. package/dist/slices/loudness/loudnorm.js +2 -1
  152. package/dist/slices/loudness/model.js +3 -2
  153. package/dist/slices/loudness/sentence-level.js +2 -1
  154. package/dist/slices/model-checks/rules.js +13 -0
  155. package/dist/slices/model-upkeep/model.js +1 -1
  156. package/dist/slices/model-upkeep/switch.js +3 -3
  157. package/dist/slices/model-upkeep/usage.js +13 -2
  158. package/dist/slices/narration/breaths.js +2 -1
  159. package/dist/slices/narration/clean-pieces.js +2 -1
  160. package/dist/slices/narration/pauses-model.js +3 -2
  161. package/dist/slices/narration/pronunciation.js +3 -2
  162. package/dist/slices/notifications/subject.js +2 -2
  163. package/dist/slices/onboarding/copy.js +19 -2
  164. package/dist/slices/onboarding/first-run.js +13 -38
  165. package/dist/slices/onboarding/install.js +2 -1
  166. package/dist/slices/onboarding/model.js +26 -21
  167. package/dist/slices/onboarding/preferred-model.js +14 -0
  168. package/dist/slices/onboarding/quick-short.js +103 -64
  169. package/dist/slices/onboarding/sample.js +4 -1
  170. package/dist/slices/onboarding/state.js +7 -0
  171. package/dist/slices/onboarding/template.js +6 -5
  172. package/dist/slices/outputs/add.js +3 -2
  173. package/dist/slices/outputs/service.js +2 -2
  174. package/dist/slices/patch-notes/library.js +1 -1
  175. package/dist/slices/play-drafts/readiness.js +3 -3
  176. package/dist/slices/play-drafts/review-inputs.js +1 -1
  177. package/dist/slices/play-drafts/service.js +17 -7
  178. package/dist/slices/project-templates/next-chapter.js +2 -1
  179. package/dist/slices/project-templates/provider-defaults.js +22 -0
  180. package/dist/slices/project-templates/setup-sections.js +26 -0
  181. package/dist/slices/project-templates/setup.js +6 -1
  182. package/dist/slices/rebuild/preview-details.js +5 -5
  183. package/dist/slices/rebuild/recipe-preparation.js +1 -1
  184. package/dist/slices/rebuild/recipe-save.js +2 -2
  185. package/dist/slices/rebuild/recipe-validation.js +5 -4
  186. package/dist/slices/rebuild/recipe-voices.js +3 -2
  187. package/dist/slices/rebuild/recipe-work.js +4 -4
  188. package/dist/slices/rebuild/recovery.js +6 -6
  189. package/dist/slices/rebuild/remake-outdated.js +5 -5
  190. package/dist/slices/rebuild/review-redo.js +1 -1
  191. package/dist/slices/rebuild/runtime-animate.js +2 -2
  192. package/dist/slices/rebuild/runtime-article.js +4 -3
  193. package/dist/slices/rebuild/runtime-document.js +5 -4
  194. package/dist/slices/rebuild/runtime-export-bed.js +2 -1
  195. package/dist/slices/rebuild/runtime-export-edit.js +2 -1
  196. package/dist/slices/rebuild/runtime-export-inputs.js +5 -4
  197. package/dist/slices/rebuild/runtime-export-short.js +1 -1
  198. package/dist/slices/rebuild/runtime-export.js +4 -3
  199. package/dist/slices/rebuild/runtime-image.js +4 -3
  200. package/dist/slices/rebuild/runtime-local.js +3 -2
  201. package/dist/slices/rebuild/runtime-narration-text.js +3 -2
  202. package/dist/slices/rebuild/runtime-provider.js +7 -6
  203. package/dist/slices/rebuild/runtime-review.js +6 -5
  204. package/dist/slices/rebuild/runtime-shorts.js +6 -5
  205. package/dist/slices/rebuild/runtime-store.js +17 -2
  206. package/dist/slices/rebuild/runtime-subtitles.js +13 -12
  207. package/dist/slices/rebuild/runtime-voices.js +2 -1
  208. package/dist/slices/rebuild/runtime-youtube.js +3 -2
  209. package/dist/slices/rebuild/service-readiness.js +11 -3
  210. package/dist/slices/research/synthesis.js +3 -2
  211. package/dist/slices/reviews/external-model.js +1 -1
  212. package/dist/slices/reviews/holds.js +5 -4
  213. package/dist/slices/reviews/model.js +8 -5
  214. package/dist/slices/reviews/rules.js +3 -3
  215. package/dist/slices/reviews/verdict.js +2 -1
  216. package/dist/slices/revisions/image-plan.js +1 -1
  217. package/dist/slices/revisions/mutation-assets.js +2 -1
  218. package/dist/slices/revisions/mutations.js +3 -2
  219. package/dist/slices/schedules/agenda.js +7 -5
  220. package/dist/slices/schedules/design-check.js +4 -4
  221. package/dist/slices/schedules/design-run.js +3 -3
  222. package/dist/slices/schedules/repo.js +6 -5
  223. package/dist/slices/schedules/schema.js +3 -0
  224. package/dist/slices/schedules/topics.js +5 -5
  225. package/dist/slices/settings/first-run.js +6 -32
  226. package/dist/slices/settings/health.js +30 -15
  227. package/dist/slices/settings/key-test.js +1 -1
  228. package/dist/slices/settings/model.js +4 -2
  229. package/dist/slices/settings/playback.js +2 -2
  230. package/dist/slices/shorts/keep.js +2 -1
  231. package/dist/slices/shorts/pick.js +4 -3
  232. package/dist/slices/shorts/prompts.js +2 -1
  233. package/dist/slices/shorts/render.js +3 -2
  234. package/dist/slices/social/media.js +5 -4
  235. package/dist/slices/social/pack.js +1 -1
  236. package/dist/slices/social/post-prompt.js +2 -1
  237. package/dist/slices/social/reshape.js +2 -2
  238. package/dist/slices/social/serve.js +1 -1
  239. package/dist/slices/social/service.js +3 -2
  240. package/dist/slices/social/state.js +1 -1
  241. package/dist/slices/storage/backup-import.js +2 -2
  242. package/dist/slices/storage/portable.js +1 -1
  243. package/dist/slices/storage/trim.js +23 -13
  244. package/dist/slices/studio/calendar.js +3 -1
  245. package/dist/slices/studio/disclosure.js +4 -4
  246. package/dist/slices/studio/model.js +1 -1
  247. package/dist/slices/studio/pack.js +9 -8
  248. package/dist/slices/studio/plan.js +1 -1
  249. package/dist/slices/studio/settings.js +6 -6
  250. package/dist/slices/studio/videos.js +23 -4
  251. package/dist/slices/style-preview/render.js +3 -3
  252. package/dist/slices/style-preview/schema.js +1 -1
  253. package/dist/slices/subtitles/captions.js +1 -1
  254. package/dist/slices/template-studio/anatomy-view.js +46 -3
  255. package/dist/slices/template-studio/bakeoff.js +3 -3
  256. package/dist/slices/template-studio/budget.js +6 -3
  257. package/dist/slices/template-studio/cards.js +78 -0
  258. package/dist/slices/template-studio/checks.js +99 -50
  259. package/dist/slices/template-studio/for-all.js +5 -5
  260. package/dist/slices/template-studio/free-video.js +1 -1
  261. package/dist/slices/template-studio/free.js +5 -5
  262. package/dist/slices/template-studio/model.js +19 -15
  263. package/dist/slices/template-studio/pack-import.js +14 -14
  264. package/dist/slices/template-studio/pack.js +9 -7
  265. package/dist/slices/template-studio/paid.js +12 -5
  266. package/dist/slices/template-studio/quote.js +18 -11
  267. package/dist/slices/template-studio/ready.js +7 -7
  268. package/dist/slices/template-studio/runner.js +1 -1
  269. package/dist/slices/template-studio/save.js +12 -12
  270. package/dist/slices/template-studio/say-it-as.js +3 -3
  271. package/dist/slices/template-studio/schedule-gate.js +1 -1
  272. package/dist/slices/template-studio/schedule-price.js +3 -1
  273. package/dist/slices/template-studio/schedule-sample.js +5 -1
  274. package/dist/slices/template-studio/schema.js +25 -0
  275. package/dist/slices/template-studio/service.js +3 -1
  276. package/dist/slices/template-studio/status.js +26 -9
  277. package/dist/slices/template-studio/topic.js +3 -3
  278. package/dist/slices/thumbnail/by-llm.js +1 -1
  279. package/dist/slices/uploads/repo.js +37 -1
  280. package/dist/slices/uploads/rules.js +3 -2
  281. package/dist/slices/video/edit-list.js +2 -2
  282. package/dist/slices/video/edit-settings.js +1 -1
  283. package/dist/slices/video/encoding.js +14 -5
  284. package/dist/slices/video/ffmpeg.js +2 -2
  285. package/dist/slices/video/plan.js +2 -1
  286. package/dist/slices/video/slideshow.js +1 -1
  287. package/dist/slices/voices/attribution.js +3 -2
  288. package/dist/slices/voices/model.js +9 -8
  289. package/dist/slices/voices/script.js +2 -1
  290. package/dist/slices/youtube/answer.js +2 -1
  291. package/dist/slices/youtube/edits-repo.js +1 -1
  292. package/dist/tutorials/AI-CLIs.md +25 -25
  293. package/dist/tutorials/Agents-MCP.md +32 -32
  294. package/dist/tutorials/Assistant.md +5 -5
  295. package/dist/tutorials/Backups.md +7 -7
  296. package/dist/tutorials/Calendar.md +17 -25
  297. package/dist/tutorials/Cast-Library.md +19 -19
  298. package/dist/tutorials/Channels.md +40 -35
  299. package/dist/tutorials/Costs-and-Run-Cost.md +38 -40
  300. package/dist/tutorials/Create-Article.md +114 -0
  301. package/dist/tutorials/Create-Extras.md +112 -0
  302. package/dist/tutorials/Create-Images.md +165 -0
  303. package/dist/tutorials/Create-Narration.md +178 -0
  304. package/dist/tutorials/Create-Overview.md +244 -0
  305. package/dist/tutorials/Create-Video-and-Style.md +144 -0
  306. package/dist/tutorials/Docker.md +3 -3
  307. package/dist/tutorials/Document-Themes.md +32 -32
  308. package/dist/tutorials/Editing-a-Project.md +94 -94
  309. package/dist/tutorials/FAQ.md +18 -18
  310. package/dist/tutorials/First-Launch-and-Welcome.md +50 -64
  311. package/dist/tutorials/Home-and-Projects.md +101 -76
  312. package/dist/tutorials/Home.md +33 -33
  313. package/dist/tutorials/Install.md +13 -13
  314. package/dist/tutorials/Intros-and-Outros.md +33 -34
  315. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +62 -58
  316. package/dist/tutorials/Keywords-Slopify-Fills.md +21 -21
  317. package/dist/tutorials/Models.md +27 -27
  318. package/dist/tutorials/Multiple-Voices.md +41 -40
  319. package/dist/tutorials/Narration-Aliases-and-Glossary.md +14 -14
  320. package/dist/tutorials/Notifications.md +21 -17
  321. package/dist/tutorials/Other-Languages.md +15 -15
  322. package/dist/tutorials/PDF-Documents.md +38 -29
  323. package/dist/tutorials/Posting-to-Social-Sites.md +12 -12
  324. package/dist/tutorials/Project-Page.md +118 -120
  325. package/dist/tutorials/Prompts-and-Templates.md +84 -0
  326. package/dist/tutorials/Prompts.md +45 -45
  327. package/dist/tutorials/Providers-and-Keys.md +105 -98
  328. package/dist/tutorials/Publishing-to-YouTube.md +31 -31
  329. package/dist/tutorials/Recovery-and-Retries.md +31 -31
  330. package/dist/tutorials/Remote-Access.md +2 -2
  331. package/dist/tutorials/Reviews-and-Checkpoints.md +90 -93
  332. package/dist/tutorials/Schedules.md +27 -28
  333. package/dist/tutorials/Settings-Reference.md +182 -204
  334. package/dist/tutorials/Shorts.md +34 -33
  335. package/dist/tutorials/Start-at-Login.md +3 -3
  336. package/dist/tutorials/Studio-Extension.md +22 -24
  337. package/dist/tutorials/Template-Studio.md +45 -42
  338. package/dist/tutorials/Templates.md +50 -79
  339. package/dist/tutorials/Trash.md +4 -4
  340. package/dist/tutorials/Troubleshooting.md +44 -44
  341. package/dist/tutorials/Uninstalling-and-Moving.md +5 -5
  342. package/dist/tutorials/Updating-and-Patch-Notes.md +18 -18
  343. package/dist/tutorials/Video-Editing.md +34 -34
  344. package/dist/tutorials/Where-Your-Files-Live.md +10 -10
  345. package/dist/tutorials/YouTube-Description.md +26 -26
  346. package/dist/tutorials/Your-First-Short.md +35 -35
  347. package/dist/tutorials/_Sidebar.md +15 -15
  348. package/dist/web/assets/index-BNftKQrA.css +1 -0
  349. package/dist/web/assets/index-Cpa58woP.js +459 -0
  350. package/dist/web/assets/pdf-BKh8f7R6.js +56 -0
  351. package/dist/web/assets/pdf.worker.min-BmVo14Nb.mjs +28 -0
  352. package/dist/web/assets/pdf.worker.min-D7i8o3wR.js +1 -0
  353. package/dist/web/index.html +2 -2
  354. package/package.json +2 -2
  355. package/dist/tutorials/Library-Overview.md +0 -76
  356. package/dist/tutorials/Play-Images.md +0 -165
  357. package/dist/tutorials/Play-Narration.md +0 -167
  358. package/dist/tutorials/Play-Outputs.md +0 -109
  359. package/dist/tutorials/Play-Overview.md +0 -227
  360. package/dist/tutorials/Play-Title-and-Article.md +0 -136
  361. package/dist/tutorials/Play-Video-and-Style.md +0 -143
  362. package/dist/web/assets/index-CkAI_2E7.css +0 -1
  363. package/dist/web/assets/index-DcD0cdfo.js +0 -334
  364. package/dist/web/assets/pdf-TODRwF31.js +0 -54
  365. package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +0 -29
  366. package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
3
  import { internalError } from "../../kernel/errors.js";
4
+ import { editSettings } from "../../kernel/ports/settings-places.js";
4
5
  import { narrationAliasesOf, usesNarrationPreparation, usesPronunciationGlossary, } from "../admission/rules.js";
5
6
  import { plainText } from "../article/plain.js";
6
7
  import { splitEndMatter } from "../article/split.js";
@@ -52,18 +53,18 @@ export async function narrationAudio(deps, view, options = {}) {
52
53
  ? one.output.role === "audio_levelled" && one.workKey === levelKey(segment)
53
54
  : one.output.role === role));
54
55
  if (row === undefined && levelled)
55
- throw new Error(`The levelled ${segment === "body" ? "narration" : segment} isn't finished yet, so the sound can't be exported at an even volume. Let the Narration stage finish (Resume, or Try again on it), then retry this stage; or turn off Level the volume in Edit project → Pauses and volume.`);
56
+ throw new Error(`The levelled ${segment === "body" ? "narration" : segment} isn't finished yet, so the sound can't be exported at an even volume. Let the Narration stage finish (Continue the run, or Try again on Narration), then try this stage again; or turn off Level the volume in ${editSettings.sound}.`);
56
57
  if (row === undefined)
57
58
  return undefined;
58
59
  const path = outputPath(deps.paths, view.revision.projectId, row.output.path);
59
60
  const duration = row.output.durationMs ?? (await deps.measure(path));
60
61
  if (!Number.isFinite(duration) || duration <= 0)
61
- throw new Error("Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Regenerate the narration (or upload your audio file again in Edit project → Narration), then Try again.");
62
+ throw new Error(`Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Press Make the narration again in the project's Narration section (or upload your audio file again in ${editSettings.narration}), then Try again.`);
62
63
  return { path, seconds: duration / 1000 };
63
64
  };
64
65
  const body = await input("audio_body");
65
66
  if (body === undefined)
66
- throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Resume, or Try again on it), then retry this stage.");
67
+ throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Continue the run, or Try again on Narration), then try this stage again.");
67
68
  return narrationTimeline(config, {
68
69
  body,
69
70
  intro: speaksPart(config, "intro") ? await input("audio_intro") : undefined,
@@ -108,7 +109,7 @@ export function revisionTranscript(deps, snapshot, kind) {
108
109
  Array.isArray(recipe.input.semantic) &&
109
110
  typeof recipe.input.semantic[0] === "string")
110
111
  return recipe.input.semantic[0];
111
- throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or Try again on it), or regenerate the missing chunk in Edit project → Narration.");
112
+ throw new Error(`Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Continue the run, or Try again on Narration), or make the missing chunk again in ${editSettings.narration}.`);
112
113
  });
113
114
  if (texts.length > 0)
114
115
  return texts.join("\n");
@@ -19,7 +19,7 @@ export async function executeShortExport(deps, context, piece, snapshot, timelin
19
19
  const words = readTimingWords(deps, context, view, "the shorts are cut and captioned from").words;
20
20
  const seconds = timeline.reduce((sum, segment) => sum + segment.seconds, 0);
21
21
  if (seconds <= 0)
22
- throw new Error("The short has no narration to play, so there is nothing to render. Let the Narration stage finish (Resume, or Try again on it), then retry this stage.");
22
+ throw new Error("The short has no narration to play, so there is nothing to render. Let the Narration stage finish (Continue the run, or Try again on Narration), then try this stage again.");
23
23
  const font = await resolveBoldFont(deps.paths, config.subtitles?.fontId ?? "default");
24
24
  if (!context.maySubmit(piece.id))
25
25
  return "held";
@@ -1,6 +1,7 @@
1
1
  import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import { extname, join, relative, sep } from "node:path";
3
3
  import { internalError } from "../../kernel/errors.js";
4
+ import { editSettings } from "../../kernel/ports/settings-places.js";
4
5
  import { usesShortMode } from "../admission/short-mode.js";
5
6
  import { masterFile, masterReport } from "../loudness/loudnorm.js";
6
7
  import { masterGoal } from "../loudness/model.js";
@@ -46,7 +47,7 @@ export async function executeExportRecipe(deps, context, piece) {
46
47
  if (!wav && piece.key !== "export:video")
47
48
  throw new Error(internalError("unknown kind of export"));
48
49
  if (wav && audio.length === 0)
49
- throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then Try again.");
50
+ throw new Error(`The audio export has nothing to export because narration is turned off for this project. Turn narration on (Narration source in ${editSettings.narration}), then Try again.`);
50
51
  if (!wav && usesShortMode(view.revision.config))
51
52
  return executeShortExport(deps, context, piece, snapshot, audio);
52
53
  const filename = wav ? "audio.wav" : "video.mp4";
@@ -240,7 +241,7 @@ export function slideshowImages(deps, context, view) {
240
241
  return view.revision.content.imageOrder.map((key) => {
241
242
  const row = readyOutput(view, { workKey: `image:${key}` });
242
243
  if (row === undefined)
243
- throw new Error("The video export can't find one of the slideshow images. Use More → make it again in its section on Images (or regenerate that image in Edit project → Images), then Try again.");
244
+ throw new Error(`The video export can't find one of the slideshow images. Press Make this image again on it in the project's Images section (or replace it in ${editSettings.images}), then Try again.`);
244
245
  return outputPath(deps.paths, context.work.projectId, row.output.path);
245
246
  });
246
247
  }
@@ -249,7 +250,7 @@ function captionDirectory(deps, context, snapshot) {
249
250
  const ass = find("subtitle_ass");
250
251
  const font = find("subtitle_font");
251
252
  if (ass === undefined || font === undefined)
252
- throw new Error("Burned-in captions are missing their caption file or font. Use More → Render the video again in the Video section (or choose the caption font again in Edit project → Subtitles), then Try again.");
253
+ throw new Error(`Burned-in captions are missing their caption file or font. Press Make the video again in the project's Video section (or choose the caption font again in ${editSettings.captions}), then Try again.`);
253
254
  const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
254
255
  try {
255
256
  mkdirSync(join(directory, "fonts"), { mode: 0o700 });
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { editSettings } from "../../kernel/ports/settings-places.js";
2
3
  import { imageBlob } from "../channels/repo.js";
3
4
  import { assetPathOrUndefined } from "../revisions/mutation-assets.js";
4
5
  import { outputPath } from "../storage/layout.js";
@@ -22,7 +23,7 @@ function referenceImage(deps, projectId, input) {
22
23
  if (input.reference === undefined)
23
24
  return undefined;
24
25
  // The step waits for the establishing image, so a missing one went away since.
25
- const missing = "The establishing image this image is drawn from is missing. Use Regenerate on the establishing image in the Images section (or upload it again in Edit project → Images), then use Try again.";
26
+ const missing = `The establishing image this image is drawn from is missing. Press Make the establishing image again in the project's Images section (or upload it again in ${editSettings.images}), then use Try again.`;
26
27
  if (input.reference.assetId === null)
27
28
  throw new Error(missing);
28
29
  const saved = assetPathOrUndefined(deps, projectId, input.reference.assetId);
@@ -37,7 +38,7 @@ function referenceImage(deps, projectId, input) {
37
38
  }
38
39
  const mime = imageMime(bytes);
39
40
  if (mime === undefined)
40
- throw new Error("The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG in Edit project → Images → Establishing image, then use Try again.");
41
+ throw new Error(`The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG under Establishing image in ${editSettings.images}, then use Try again.`);
41
42
  return { bytes, mime };
42
43
  }
43
44
  // The cast pictures the request names, read by hash from the database. They are never removed
@@ -51,7 +52,7 @@ function castImages(deps, input) {
51
52
  images: member.images.map((sha256) => {
52
53
  const blob = imageBlob(deps.db, sha256);
53
54
  if (blob === undefined)
54
- throw new Error(`A picture of ${member.name} that this project was started with is not in this Slopify's database, so it can't be sent as a reference. Add the picture again in Channels → Cast, then start the video again from Play.`);
55
+ throw new Error(`A picture of ${member.name} that this project was started with is not in this Slopify's database, so it can't be sent as a reference. Add the picture again in Channels → Cast, then start the video again from Create.`);
55
56
  return blob;
56
57
  }),
57
58
  }));
@@ -2,6 +2,7 @@ import { rmSync, statSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { internalError } from "../../kernel/errors.js";
5
+ import { editSettings } from "../../kernel/ports/settings-places.js";
5
6
  import { referenceKey, thumbnailVariant } from "../admission/model.js";
6
7
  import { plainText } from "../article/plain.js";
7
8
  import { splitEndMatter } from "../article/split.js";
@@ -31,7 +32,7 @@ export async function executeLocalRecipe(deps, context, piece) {
31
32
  const input = piece.input;
32
33
  if (input.kind === "provided") {
33
34
  if (input.assetId === null)
34
- throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then Try again.");
35
+ throw new Error("A file you uploaded for this project is missing. Upload it again in Edit settings, then Try again.");
35
36
  const row = z
36
37
  .object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
37
38
  .parse(deps.db
@@ -115,7 +116,7 @@ async function concatenate(deps, context, piece) {
115
116
  isDonePiece(candidate) &&
116
117
  candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
117
118
  if (input?.assetId === null || input === undefined)
118
- throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then Try again.");
119
+ throw new Error(`One narration chunk has no saved audio, so the narration can't be joined together. Make the missing chunk again in ${editSettings.narration}, then Try again.`);
119
120
  return outputPath(deps.paths, context.work.projectId, assetPath(deps, context.work.projectId, input.assetId));
120
121
  });
121
122
  // Level the volume repeats the join its recipe names, from the same pieces, each levelled
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { internalError } from "../../kernel/errors.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { isDonePiece } from "../revisions/view.js";
4
5
  import { executionPlan, executionView, savedCatalogue, workCatalogue } from "./runtime-plan.js";
5
6
  import { preparedTexts, publishResult } from "./runtime-publication.js";
@@ -16,7 +17,7 @@ export function narrationTextParts(view, plan, segment) {
16
17
  const recipe = plan.recipes.find((row) => row.key === key);
17
18
  const selected = view.pieces.find((row) => row.key === key && isDonePiece(row) && row.fingerprint === recipe?.fingerprint);
18
19
  if (selected === undefined || recipe === undefined)
19
- throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or Try again on it), or regenerate the missing chunk in Edit project → Narration.");
20
+ throw new Error(`Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Continue the run, or Try again on Narration), or make the missing chunk again in ${editSettings.narration}.`);
20
21
  const input = recipe.input;
21
22
  if (input.kind === "provided" &&
22
23
  Array.isArray(input.semantic) &&
@@ -33,7 +34,7 @@ export function narrationTextParts(view, plan, segment) {
33
34
  .parse(JSON.parse(selected.piece.payload ?? "{}"));
34
35
  if (input.spokenText !== undefined &&
35
36
  (payload.spokenText !== input.spokenText || payload.text !== input.text))
36
- throw new Error("A narration chunk's audio no longer matches its prepared text. Regenerate that chunk in Edit project → Narration, then Try again.");
37
+ throw new Error(`A narration chunk's audio no longer matches its prepared text. Make that chunk again in ${editSettings.narration}, then Try again.`);
37
38
  return {
38
39
  logicalKey: input.logicalKey,
39
40
  spokenText: input.spokenText === undefined ? (payload.text ?? input.text) : (payload.spokenText ?? ""),
@@ -1,5 +1,6 @@
1
1
  import { basename } from "node:path";
2
2
  import { internalError } from "../../kernel/errors.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { referenceKey, thumbnailVariant } from "../admission/model.js";
4
5
  import { plainText } from "../article/plain.js";
5
6
  import { splitEndMatter } from "../article/split.js";
@@ -189,17 +190,17 @@ export async function softenIfAsked(deps, context, providers, piece, prompt, lab
189
190
  messages: softenMessages(prompt),
190
191
  previewLabel: `${label}: softened prompt`,
191
192
  check: (value) => softenedPrompt(value.text) === ""
192
- ? "The AI model sent back an empty prompt while softening it. Use Soften and retry again, or reword the prompt yourself in Edit project → Images."
193
+ ? `The AI model sent back an empty prompt while softening it. Use Soften and retry again, or reword the prompt yourself in ${editSettings.images}.`
193
194
  : undefined,
194
195
  });
195
196
  return answer.ok ? softenedPrompt(answer.value.text) : "held";
196
197
  }
197
198
  function checkAnswer(piece, answer) {
198
199
  if (answer.text.trim() === "")
199
- return "The AI model sent back an empty answer. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project.";
200
+ return `The AI model sent back an empty answer. Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`;
200
201
  if (piece.input.kind === "llm" && piece.input.describe !== undefined)
201
202
  return spokenPassage(answer.text) === ""
202
- ? "The AI model's description had no words a narrator could say. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project, or turn off Describe tables and figures there."
203
+ ? `The AI model's description had no words a narrator could say. Use Try again; if it keeps happening, choose another model in ${editSettings.writer}, or turn off Describe tables and figures in ${editSettings.narration}.`
203
204
  : undefined;
204
205
  // Scenes from the article: one scene per image, as many as the request lists.
205
206
  if ((piece.key === imageScenesKey || piece.key === thumbnailScenesKey) &&
@@ -222,7 +223,7 @@ function checkAnswer(piece, answer) {
222
223
  return checkPost(answer.text, piece.input.social);
223
224
  if (piece.key === "research:planner")
224
225
  return chaptersFrom(answer.text).length === 0
225
- ? "The AI model's research plan listed no chapters, so research could not go on. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project."
226
+ ? `The AI model's research plan listed no chapters, so research could not go on. Use Try again; if it keeps happening, choose another model in ${editSettings.writer}.`
226
227
  : undefined;
227
228
  if (piece.key === "research:notes" || piece.key.startsWith("research:chapter:"))
228
229
  return sourcedAnswer(piece.key, answer.text);
@@ -236,7 +237,7 @@ function checkAnswer(piece, answer) {
236
237
  const checked = parseScript(answer.text, piece.input.script.speakers);
237
238
  return checked.ok
238
239
  ? undefined
239
- : `The conversation the text model wrote can't be read as a script: ${checked.reason}`;
240
+ : `The conversation the AI model wrote can't be read as a script: ${checked.reason}`;
240
241
  }
241
242
  return undefined;
242
243
  }
@@ -322,7 +323,7 @@ async function publishText(deps, context, piece, answer) {
322
323
  if (piece.input.kind === "llm" && piece.input.script !== undefined) {
323
324
  const checked = parseScript(parts.body, piece.input.script.speakers);
324
325
  if (!checked.ok)
325
- throw new Error(`The text model's script can't be read: ${checked.reason} Try again to have it written again, or fix it in Edit project → Article.`);
326
+ throw new Error(`The AI model's script can't be read: ${checked.reason} Try again to have it written again, or fix it in ${editSettings.article}.`);
326
327
  }
327
328
  await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
328
329
  ["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
3
  import { internalError } from "../../kernel/errors.js";
4
4
  import { thinkingModes } from "../../kernel/ports/llm.js";
5
+ import { editSettings } from "../../kernel/ports/settings-places.js";
5
6
  import { plainText } from "../article/plain.js";
6
7
  import { defaultReviewPrompts, narrationItemKey, reviewStages, } from "../reviews/model.js";
7
8
  import { nextAttempt, reviewOutcome } from "../reviews/outcome.js";
@@ -191,8 +192,8 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
191
192
  const image = output(itemKey);
192
193
  if (image === undefined)
193
194
  throw missing(stage === "images" ? "image" : "thumbnail", stage === "images"
194
- ? "Use More → make it again in its section on Images"
195
- : "Use More → make it again in its section on Thumbnail");
195
+ ? "Press Make this image again on it in the project's Images section"
196
+ : "Press Make the thumbnail again in the project's Images section");
196
197
  const recipe = recipes.find((one) => one.key === itemKey);
197
198
  const brief = recipe?.input.kind === "image" ? recipe.input.prompt : "";
198
199
  const reference = stage === "images" ? output("reference:image") : undefined;
@@ -226,7 +227,7 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
226
227
  case "narration": {
227
228
  const timing = output("subtitles:timing", "subtitle_words");
228
229
  if (timing === undefined || itemKey !== narrationItemKey)
229
- throw missing("narration's word timing", "Use More → Render the video again in the Video section");
230
+ throw missing("narration's word timing", "Press Make the video again in the project's Video section");
230
231
  const { words, omissions } = wordsSchema.parse(JSON.parse(readFileSync(path(timing), "utf8")));
231
232
  const unsure = words.filter((word) => (word.confidence ?? 1) < 0.3).length;
232
233
  return {
@@ -254,7 +255,7 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
254
255
  case "shorts": {
255
256
  const render = output(`${itemKey}:render`);
256
257
  if (render === undefined)
257
- throw missing("short", "Use Make this short again in Edit project → Shorts");
258
+ throw missing("short", `Use Make this short again in ${editSettings.shorts}`);
258
259
  const number = Number(itemKey.slice("shorts:".length));
259
260
  const pick = view.pieces.find((row) => row.key === "shorts:pick" && row.selected && row.piece.state === "done");
260
261
  const title = pickedShortsOf(pick?.piece.payload)?.shorts.find((clip) => clip.number === number)?.title ??
@@ -263,7 +264,7 @@ export function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt)
263
264
  .filter((row) => row.workKey.startsWith(`${itemKey}:image:`) && isReadyOutput(row))
264
265
  .slice(0, 6);
265
266
  if (stills.length === 0)
266
- throw missing("short's images", "Use Make this short again in Edit project → Shorts");
267
+ throw missing("short's images", `Use Make this short again in ${editSettings.shorts}`);
267
268
  const names = stills.map((_, index) => `image ${String(index + 1)} of the short`);
268
269
  return {
269
270
  request: request([{ label: "Title of the short", text: title }], names),
@@ -1,5 +1,6 @@
1
1
  import { errorText, internalError } from "../../kernel/errors.js";
2
2
  import { withLanguage, wordTimingUnavailable } from "../../kernel/ports/languages.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { usesFigureCards, usesShorts } from "../admission/rules.js";
4
5
  import { captionBoldFont } from "../fonts/coverage.js";
5
6
  import { masterGoal } from "../loudness/model.js";
@@ -48,7 +49,7 @@ export async function executeShortsRecipe(deps, context, providers, piece) {
48
49
  throw setupError();
49
50
  const clip = savedClips(snapshot).find((one) => one.number === number);
50
51
  if (clip === undefined)
51
- throw new Error("The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in Edit project → Shorts, then Try again.");
52
+ throw new Error(`The clip this short was cut from is no longer among the picked shorts. Use Pick different moments in ${editSettings.shorts}, then Try again.`);
52
53
  if (matched[2] === "prompts")
53
54
  return prompts(deps, context, providers, piece, snapshot, clip);
54
55
  if (matched[2] === "render")
@@ -66,10 +67,10 @@ async function pick(deps, context, providers, piece, snapshot) {
66
67
  throw setupError();
67
68
  const llm = config.llm;
68
69
  if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
69
- throw new Error("Shorts need an AI text model to pick the clips, and none is chosen. Choose one in Edit project → Providers, then Try again.");
70
+ throw new Error(`Shorts need an AI model to pick the clips, and none is chosen. Choose a Service and a Model in ${editSettings.writer}, then Try again.`);
70
71
  const sentences = transcriptSentences(readTimingWords(deps, context, view, shortsTimingPurpose).words);
71
72
  if (sentences.length === 0)
72
- throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Use More → Render the video again in the Video section, then Try again.");
73
+ throw new Error("The narration's word timing holds no words, so there is nothing to cut shorts from. Press Make the video again in the project's Video section, then Try again.");
73
74
  const audio = await revisionAudio(deps, context, view);
74
75
  const durationSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
75
76
  // Kept shorts are found again in this timing; the model is asked only for the rest.
@@ -238,7 +239,7 @@ async function render(deps, context, piece, snapshot, clip) {
238
239
  .map((recipe) => {
239
240
  const row = readyOutput(view, { workKey: recipe.key });
240
241
  if (row === undefined)
241
- throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in Edit project → Shorts.`);
242
+ throw new Error(`Short ${String(clip.number)} is missing one of its images. Try again to make it again; if it keeps happening, use Make this short again in ${editSettings.shorts}.`);
242
243
  return outputPath(deps.paths, context.work.projectId, row.output.path);
243
244
  });
244
245
  const allWords = readTimingWords(deps, context, view, shortsTimingPurpose).words;
@@ -344,7 +345,7 @@ async function backgroundMusic(deps, context, view) {
344
345
  const shorts = view.revision.config.shorts;
345
346
  if (assetId === undefined || shorts === undefined)
346
347
  return undefined;
347
- const fix = "Choose another file under Background music in Edit project → Shorts, or remove the music, then Try again.";
348
+ const fix = `Choose another file under Background music in ${editSettings.shorts}, or remove the music, then Try again.`;
348
349
  const saved = assetPathOrUndefined(deps, context.work.projectId, assetId);
349
350
  if (saved === undefined)
350
351
  throw new Error(`The shorts' background music file is no longer in the project. ${fix}`);
@@ -223,7 +223,13 @@ export function projectStandings(deps, projectId) {
223
223
  }, 0);
224
224
  // A failed step, or else one waiting to run again, says why on the stage row. A failed
225
225
  // stage keeps the moment it failed: needs_attention tells a narration retry started after
226
- // the failure (still going) from one started before it (spent) by it.
226
+ // the failure (still going) from one started before it (spent) by it. A stage's start is
227
+ // the moment it went running, kept through a wait to retry, between its steps (one done,
228
+ // the next not claimed yet) and once it ends; a pending start is kept only then, so a
229
+ // stage made again after it was done takes a new one when it runs. A done
230
+ // stage keeps the moment it finished: the project's run length, the time left on a running
231
+ // step and the usual time a step takes (`eta/history.ts`) read them, and a later save that
232
+ // changes nothing in the stage moves neither.
227
233
  const failed = all.find((work) => work.state === "failed") ??
228
234
  (stage.retryAt === undefined || stage.retryAt === null
229
235
  ? undefined
@@ -236,7 +242,16 @@ export function projectStandings(deps, projectId) {
236
242
  deps.db
237
243
  .prepare(`UPDATE stages SET state=:state,failure_reason=:reason,failure_kind=:kind,retry_at=:retryAt,
238
244
  progress_current=:current,progress_total=:total,
239
- finished_at=CASE WHEN :state='done' THEN :now
245
+ started_at=CASE
246
+ WHEN :state='running' THEN CASE
247
+ WHEN started_at IS NOT NULL AND state IN ('running','pending') THEN started_at
248
+ ELSE :now END
249
+ WHEN :state IN ('done','failed') THEN started_at
250
+ WHEN :state='pending' AND :retryAt IS NOT NULL THEN started_at
251
+ WHEN :state='pending' AND state IN ('running','pending') AND :current > 0 THEN started_at
252
+ ELSE NULL END,
253
+ finished_at=CASE
254
+ WHEN :state='done' THEN CASE WHEN state='done' AND finished_at IS NOT NULL THEN finished_at ELSE :now END
240
255
  WHEN :state='failed' THEN CASE WHEN state='failed' AND finished_at IS NOT NULL THEN finished_at ELSE :now END
241
256
  ELSE NULL END
242
257
  WHERE project_id=:projectId AND kind=:stage`)
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { internalError } from "../../kernel/errors.js";
4
4
  import { subtitleModelDir } from "../../kernel/paths.js";
5
5
  import { projectLanguage } from "../../kernel/ports/languages.js";
6
+ import { editSettings } from "../../kernel/ports/settings-places.js";
6
7
  import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
7
8
  import { narrationAliasesOf } from "../admission/rules.js";
8
9
  import { captionFont, captionFontDeps } from "../fonts/coverage.js";
@@ -54,7 +55,7 @@ const cuesSchema = z.object({
54
55
  export function readTimingWords(deps, context, view, purpose) {
55
56
  const timing = readyOutput(view, { workKey: "subtitles:timing", role: "subtitle_words" });
56
57
  if (timing === undefined)
57
- throw new Error(`The narration's word timing, which ${purpose}, is missing. Use More → Render the video again in the Video section, then Try again.`);
58
+ throw new Error(`The narration's word timing, which ${purpose}, is missing. Press Make the video again in the project's Video section, then Try again.`);
58
59
  return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8")));
59
60
  }
60
61
  export async function executeSubtitleRecipe(deps, context, piece) {
@@ -73,7 +74,7 @@ export async function executeSubtitleRecipe(deps, context, piece) {
73
74
  async function timing(deps, context, piece, snapshot) {
74
75
  const audio = await revisionAudio(deps, context, snapshot.view);
75
76
  if (audio.length === 0)
76
- throw new Error("Captions and the YouTube description need narration audio, but this project has none. Turn captions and the YouTube description off in Edit project, or turn narration on, then Try again.");
77
+ throw new Error(`Captions and the YouTube description need narration audio, but this project has none. Turn captions off in ${editSettings.captions} and the YouTube description off in ${editSettings.extras}, or turn narration on, then Try again.`);
77
78
  const alignSubtitles = deps.alignSubtitles;
78
79
  if (alignSubtitles === undefined)
79
80
  throw new Error(internalError("the caption timing tool is missing from this build"));
@@ -121,7 +122,7 @@ async function timing(deps, context, piece, snapshot) {
121
122
  offset += segment.seconds;
122
123
  }
123
124
  if (captionCues(words).length === 0)
124
- throw new Error("None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project language set in Edit project → Language; if you uploaded your own audio, make sure it reads the article text, then Try again.");
125
+ throw new Error(`None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project's language (${editSettings.language}); if you uploaded your own audio, make sure it reads the article text, then Try again.`);
125
126
  context.signal.throwIfAborted();
126
127
  const output = preparedText(deps, context, piece, "subtitle_words", "subtitles.json", JSON.stringify({ key: piece.logicalFingerprint ?? piece.fingerprint, words, omissions }));
127
128
  await publishResult(deps, context, piece, [output], { words, omissions }, output.asset);
@@ -133,7 +134,7 @@ async function cues(deps, context, piece, snapshot) {
133
134
  let value;
134
135
  if (manual !== undefined) {
135
136
  if (plan.work.find((one) => one.key === piece.key)?.disposition === "review")
136
- throw new Error("Your edited captions were written for an older version of the narration. Review them in Edit project → Captions and save, then Try again.");
137
+ throw new Error(`Your edited captions were written for an older version of the narration. Look them over in ${editSettings.captions} and save, then Try again.`);
137
138
  const audio = await revisionAudio(deps, context, view);
138
139
  const errors = validateCues(manual.cues, audio.reduce((sum, segment) => sum + segment.seconds, 0));
139
140
  if (errors.length > 0)
@@ -143,7 +144,7 @@ async function cues(deps, context, piece, snapshot) {
143
144
  else {
144
145
  const output = readyOutput(view, { workKey: "subtitles:timing" });
145
146
  if (output === undefined)
146
- throw new Error("Caption timing is missing. Use More → Render the video again in the Video section, then Try again.");
147
+ throw new Error("Caption timing is missing. Press Make the video again in the project's Video section, then Try again.");
147
148
  const timed = wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, output.output.path), "utf8")));
148
149
  value = { cues: [...captionCues(timed.words)], omissions: timed.omissions };
149
150
  }
@@ -160,7 +161,7 @@ async function files(deps, context, piece, snapshot) {
160
161
  throw new Error(internalError("captions are off for this version of the project"));
161
162
  const row = donePiece(view, "subtitles:cues");
162
163
  if (row?.piece.payload === null || row?.piece.payload === undefined)
163
- throw new Error("The captions haven't been prepared yet. Use More → Render the video again in the Video section, then Try again.");
164
+ throw new Error("The captions haven't been prepared yet. Press Make the video again in the project's Video section, then Try again.");
164
165
  const value = cuesSchema.parse(JSON.parse(row.piece.payload));
165
166
  const previous = view.outputs.find((one) => one.output.role === "subtitle_font" && one.selected && one.available);
166
167
  const previousFiles = view.pieces.find((one) => one.key === "subtitles:files" &&
@@ -318,19 +319,19 @@ retry) {
318
319
  : `${clock(mismatch.at)} into the ${segment} narration, in narration chunk ${String(at + 1)} of ${String(chunks.length)} (it starts "${opening(chunk.spokenText)}")`;
319
320
  const expected = mismatch.expected === "" ? "the end of the text" : `"${mismatch.expected}…"`;
320
321
  const heard = mismatch.heard === "" ? "no more speech" : `"${mismatch.heard.toLowerCase()}…"`;
321
- const stopped = `Subtitles stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard}`;
322
+ const stopped = `Captions stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard}`;
322
323
  if (retry !== undefined) {
323
324
  const n = String(retry.chunk);
324
325
  return retry.try === undefined
325
- ? `${stopped} Slopify already recorded narration chunk ${n} again ${String(narrationRetryLimit)} times and the audio still doesn't match there, so the voice probably reads something its own way (a year, an abbreviation or a name). Listen at ${clock(mismatch.at)}: if words are missing or wrong, reword that sentence in Edit project → Narration → narration chunk ${n}, save, then Continue the run. If it says them right, use Download diagnostics in Settings and report it.`
326
- : `${stopped} The voice probably skipped or garbled words there, so Slopify is recording narration chunk ${n} again by itself (try ${String(retry.try)} of ${String(narrationRetryLimit)}) and carries on with the video when it's done. If this message is still here in a few minutes, regenerate narration chunk ${n} in Edit project → Narration, then Continue the run.`;
326
+ ? `${stopped} Slopify already recorded narration chunk ${n} again ${String(narrationRetryLimit)} times and the audio still doesn't match there, so the voice probably reads something its own way (a year, an abbreviation or a name). Listen at ${clock(mismatch.at)}: if words are missing or wrong, reword that sentence in narration chunk ${n} in ${editSettings.narration}, save, then Continue the run. If it says them right, use Download diagnostics in Settings and report it.`
327
+ : `${stopped} The voice probably skipped or garbled words there, so Slopify is recording narration chunk ${n} again by itself (try ${String(retry.try)} of ${String(narrationRetryLimit)}) and carries on with the video when it's done. If this message is still here in a few minutes, make narration chunk ${n} again in ${editSettings.narration}, then Continue the run.`;
327
328
  }
328
329
  const fix = chunk === undefined
329
- ? `Listen at ${clock(mismatch.at)}: if words are missing or wrong, regenerate that part in Edit project → Narration, then Continue the run.`
330
- : `Listen at ${clock(mismatch.at)}: if words are missing or wrong, regenerate narration chunk ${String(at + 1)} in Edit project → Narration, then Continue the run.`;
330
+ ? `Listen at ${clock(mismatch.at)}: if words are missing or wrong, make that part again in ${editSettings.narration}, then Continue the run.`
331
+ : `Listen at ${clock(mismatch.at)}: if words are missing or wrong, make narration chunk ${String(at + 1)} again in ${editSettings.narration}, then Continue the run.`;
331
332
  // A voice that reads a year, an abbreviation or a name its own way reads it the same way
332
333
  // again, so a remake would cost a chunk and fail here once more.
333
- const same = "If it says them right, only another way (a year, an abbreviation or a name read its own way), regenerating gives the same reading: use Download diagnostics in Settings and report it.";
334
+ const same = "If it says them right, only another way (a year, an abbreviation or a name read its own way), making it again gives the same reading: use Download diagnostics in Settings and report it.";
334
335
  return `${stopped} The recording there probably skips or changes words. ${fix} ${same}`;
335
336
  }
336
337
  function comparable(text) {
@@ -2,6 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { internalError } from "../../kernel/errors.js";
5
+ import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
5
6
  import { masterFile, masterReport } from "../loudness/loudnorm.js";
6
7
  import { masterGoal } from "../loudness/model.js";
7
8
  import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
@@ -35,7 +36,7 @@ export async function executeVoicesRecipe(deps, context, piece) {
35
36
  const { words } = readTimingWords(deps, context, view, "the chapter markers come from");
36
37
  const audio = await revisionAudio(deps, context, view, { levelled: true });
37
38
  if (audio.length === 0)
38
- throw new Error("The MP3 and M4B files need narration audio, but this project has none. Turn narration on in Edit project, or turn Audio files off under Speakers (Play → Narration, or Edit project → Providers), then Try again.");
39
+ throw new Error(`The MP3 and M4B files need narration audio, but this project has none. Turn narration on in ${editSettings.narration}, or untick Also make MP3 and M4B files under Speakers (${createRows.narration}, or ${editSettings.narration}), then Try again.`);
39
40
  const totalSeconds = audio.reduce((sum, segment) => sum + segment.seconds, 0);
40
41
  const chapters = audioChapters({
41
42
  title: view.revision.config.title,
@@ -1,5 +1,6 @@
1
1
  import { internalError } from "../../kernel/errors.js";
2
2
  import { withLanguage } from "../../kernel/ports/languages.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { assembleDescription, checkDescriptionAnswer, descriptionMessages, tagsText, } from "../youtube/answer.js";
4
5
  import { defaultDescriptionPrompt } from "../youtube/model.js";
5
6
  import { keepsShape, titleShape } from "../youtube/titles.js";
@@ -26,11 +27,11 @@ export async function executeYoutubeRecipe(deps, context, providers, piece) {
26
27
  const config = view.revision.config;
27
28
  const llm = config.llm;
28
29
  if (llm === undefined || llm.provider.trim() === "" || llm.model.trim() === "")
29
- throw new Error("The YouTube description needs an AI text model, and none is chosen. Choose one in Edit project → Providers, then Try again.");
30
+ throw new Error(`The YouTube description needs an AI model, and none is chosen. Choose a Service and a Model in ${editSettings.writer}, then Try again.`);
30
31
  const { words } = readTimingWords(deps, context, view, "the YouTube description's chapters come from");
31
32
  const passages = transcriptPassages(words);
32
33
  if (passages.length === 0)
33
- throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Use More → Render the video again in the Video section, then Try again.");
34
+ throw new Error("The narration's word timing holds no words, so there is nothing to write the YouTube description from. Press Make the video again in the project's Video section, then Try again.");
34
35
  // The same timeline the timing walked: silence at the start, intro, gaps, body, outro and
35
36
  // silence at the end.
36
37
  const audio = await revisionAudio(deps, context, view);
@@ -33,9 +33,14 @@ export async function checkReadiness(deps, snapshot, view) {
33
33
  if (provider === undefined || !readinessIsUsable(provider.readiness)) {
34
34
  fields.push({
35
35
  field: choice.family,
36
+ // Said under the step's name on the project page ("Images: …"), in the person's words:
37
+ // a service Slopify does not offer here (a sample's own) is changed in the project's
38
+ // settings, one it offers is set up under Keys & services.
36
39
  message: provider !== undefined && provider.readiness.kind !== "keyed" && provider.readiness.issue
37
40
  ? provider.readiness.issue
38
- : "Configure this provider before rebuilding.",
41
+ : provider === undefined
42
+ ? "This project's service isn't one Slopify can use here. Choose another in Edit settings, then try again."
43
+ : "Its service isn't set up yet. Add it in Settings → Keys & services, then try again.",
39
44
  });
40
45
  continue;
41
46
  }
@@ -72,7 +77,10 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
72
77
  message: "The configured CLI command changed. Check readiness again before rebuilding.",
73
78
  });
74
79
  if (provider?.readiness.kind === "keyed" && !hasKey(deps.db, provider.id))
75
- fields.push({ field: choice.family, message: "Save the provider key before rebuilding." });
80
+ fields.push({
81
+ field: choice.family,
82
+ message: "Its key isn't saved yet. Add it in Settings → Keys & services, then try again.",
83
+ });
76
84
  if (choice.voice !== undefined &&
77
85
  !listVoices(deps.db).some((row) => row.provider === choice.provider && row.voiceId === choice.voice))
78
86
  fields.push({ field: "audio.voice", message: "Choose a saved voice before rebuilding." });
@@ -88,7 +96,7 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
88
96
  if (maximum !== undefined && input.text.length > maximum)
89
97
  fields.push({
90
98
  field: `work.${recipe.key}.text`,
91
- message: "This saved physical request exceeds the current model limit. Regenerate this narration group to plan new parts.",
99
+ message: "This saved request is longer than the voice model takes now. Make this part of the narration again, so it is split into parts that fit.",
92
100
  });
93
101
  }
94
102
  else if (input.kind === "llm") {
@@ -1,4 +1,5 @@
1
1
  import { documentIndex } from "../../kernel/ports/llm-documents.js";
2
+ import { editSettings } from "../../kernel/ports/settings-places.js";
2
3
  import { researchDocuments } from "./documents.js";
3
4
  import { briefText } from "./planner.js";
4
5
  // An editorial pass over every sub-agent's output that selects and organizes the findings; it
@@ -41,9 +42,9 @@ export function endsWithSources(text) {
41
42
  // but cannot be used is a failed attempt, so the wrapper retries it.
42
43
  export function sourcedAnswer(who, text) {
43
44
  if (text.trim() === "") {
44
- return `The AI model returned an empty answer for ${who}. Try again; if it keeps happening, choose a different model in Edit project → Providers.`;
45
+ return `The AI model returned an empty answer for ${who}. Try again; if it keeps happening, choose a different model in ${editSettings.writer}.`;
45
46
  }
46
47
  return endsWithSources(text)
47
48
  ? undefined
48
- : `The AI model's answer for ${who} had no sources. Research needs web sources: Try again, or choose a model with web search in Edit project → Providers.`;
49
+ : `The AI model's answer for ${who} had no sources. Research needs web sources: Try again, or choose a model with web search in ${editSettings.writer}.`;
49
50
  }
@@ -20,5 +20,5 @@ export const closedReviewSentences = {
20
20
  superseded: "This review was replaced: its item was made again or edited, and the new one waits for its own verdict. Call needs_attention to see it.",
21
21
  "replaced-pending": "This review was replaced: its item was made again or edited, so it takes no verdict, and no new review waits for it yet. One opens once the new item is made; an edit saved but not continued makes none until the run is continued. There is nothing to answer here: call needs_attention to see what is waiting now.",
22
22
  canceled: "This review's run was canceled, so nothing waits on it and it takes no verdict: a canceled run's reviews never pass. Call needs_attention to see what is waiting now.",
23
- trashed: "This review's project is in the Trash, so nothing waits on it and it takes no verdict. Restore the project in Settings → Backup & storage → Trash; the review then waits for its verdict again.",
23
+ trashed: "This review's project is in the Trash, so nothing waits on it and it takes no verdict. Restore the project in Settings → Trash; the review then waits for its verdict again.",
24
24
  };
@@ -4,7 +4,7 @@ import { pendingAdmitted } from "../admission/admitted-work.js";
4
4
  import { liveProject, oneProject, stageStandingsByProject } from "../admission/repo.js";
5
5
  import { checkpointStageSchema } from "../checkpoints/schema.js";
6
6
  import { heldForItsDay } from "../schedules/prepare.js";
7
- import { uploadedAndSettled } from "../uploads/repo.js";
7
+ import { settledProjects } from "../uploads/repo.js";
8
8
  import { projectsWaitingOnAgents } from "./external-waiting.js";
9
9
  const gateRow = z.object({
10
10
  project_id: z.string(),
@@ -18,7 +18,7 @@ const gateRow = z.object({
18
18
  const projectRow = z.object({ project_id: z.string() });
19
19
  // Every project's holds, or one project's alone when `projectId` is given. A caller that has
20
20
  // already read the stage standings (`stageStandingsByProject`) or the uploaded projects that
21
- // read Done (`uploadedAndSettled`) passes them; one that lists what it can read passes `skip`,
21
+ // read Done (`settledProjects`) passes them; one that lists what it can read passes `skip`,
22
22
  // given each checkpoint row that can't be read instead of failing them all.
23
23
  export function projectHolds(db, scope = {}) {
24
24
  const { projectId, skip } = scope;
@@ -74,8 +74,9 @@ export function projectHolds(db, scope = {}) {
74
74
  .map((row) => projectRow.parse(row).project_id);
75
75
  const agents = projectsWaitingOnAgents(db, projectId);
76
76
  const runs = heldRuns(db, projectId);
77
- // An uploaded project with nothing admitted reads Done (`edge/http/projects.ts`).
78
- const settled = scope.settled ?? uploadedAndSettled(db, projectId);
77
+ // An uploaded project with nothing admitted, or a finished one an edit left only outdated,
78
+ // reads Done (`edge/http/projects.ts`).
79
+ const settled = scope.settled ?? settledProjects(db, projectId);
79
80
  const waitingForAgent = new Set([...agents].filter((id) => !reachedTheirs.has(id) && runs.get(id)?.held !== true));
80
81
  const stopped = new Set([...runs]
81
82
  .filter(([, run]) => run.stuck)