@gentbajko/slopify 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/dist/adapters/account-limits.js +1 -1
  2. package/dist/adapters/alignment/audio.js +1 -1
  3. package/dist/adapters/alignment/cache.js +1 -1
  4. package/dist/adapters/alignment/multilingual.js +2 -1
  5. package/dist/adapters/alignment/runner.js +1 -1
  6. package/dist/adapters/alignment/worker.js +1 -1
  7. package/dist/adapters/explain.js +10 -9
  8. package/dist/adapters/host-cli/index.js +2 -1
  9. package/dist/adapters/host-cli/transport.js +2 -1
  10. package/dist/adapters/image/bytes.js +2 -1
  11. package/dist/adapters/image/codex.js +6 -5
  12. package/dist/adapters/image/fal.js +4 -3
  13. package/dist/adapters/image/google.js +3 -2
  14. package/dist/adapters/image/models.js +2 -2
  15. package/dist/adapters/image/openai.js +2 -1
  16. package/dist/adapters/image/replicate.js +3 -2
  17. package/dist/adapters/image/video.js +2 -1
  18. package/dist/adapters/key-probes.js +1 -1
  19. package/dist/adapters/llm/antigravity-models.js +1 -1
  20. package/dist/adapters/llm/antigravity.js +7 -6
  21. package/dist/adapters/llm/assistant-cli.js +1 -1
  22. package/dist/adapters/llm/claude-code-models.js +1 -1
  23. package/dist/adapters/llm/claude-code.js +3 -2
  24. package/dist/adapters/llm/codex-models.js +1 -1
  25. package/dist/adapters/llm/document-workspace.js +3 -2
  26. package/dist/adapters/llm/image-workspace.js +3 -2
  27. package/dist/adapters/llm/openrouter.js +2 -1
  28. package/dist/adapters/llm/run-cli.js +3 -2
  29. package/dist/adapters/tts/gemini.js +2 -1
  30. package/dist/adapters/tts/inworld-async.js +2 -1
  31. package/dist/adapters/tts/inworld.js +3 -2
  32. package/dist/adapters/tts/system.js +2 -1
  33. package/dist/catalog/registry.js +20 -13
  34. package/dist/catalog/schema.js +2 -1
  35. package/dist/catalog/validate.js +5 -2
  36. package/dist/edge/http/actions.js +7 -7
  37. package/dist/edge/http/app.js +5 -3
  38. package/dist/edge/http/assistant.js +3 -1
  39. package/dist/edge/http/backups.js +1 -1
  40. package/dist/edge/http/channel-memory.js +2 -2
  41. package/dist/edge/http/channels.js +1 -1
  42. package/dist/edge/http/checkpoints.js +7 -5
  43. package/dist/edge/http/design-templates.js +3 -3
  44. package/dist/edge/http/designs.js +1 -1
  45. package/dist/edge/http/document-themes.js +3 -3
  46. package/dist/edge/http/draft-problem.js +1 -1
  47. package/dist/edge/http/extension-auth.js +1 -1
  48. package/dist/edge/http/home.js +9 -1
  49. package/dist/edge/http/model-checks.js +4 -4
  50. package/dist/edge/http/onboarding.js +29 -12
  51. package/dist/edge/http/outputs.js +3 -2
  52. package/dist/edge/http/patch-notes.js +2 -2
  53. package/dist/edge/http/planning.js +1 -1
  54. package/dist/edge/http/project-create.js +1 -1
  55. package/dist/edge/http/project-templates.js +2 -2
  56. package/dist/edge/http/projects.js +22 -4
  57. package/dist/edge/http/prompts.js +2 -2
  58. package/dist/edge/http/pronunciation-check.js +133 -0
  59. package/dist/edge/http/pronunciations.js +26 -0
  60. package/dist/edge/http/providers.js +9 -10
  61. package/dist/edge/http/refusal.js +1 -1
  62. package/dist/edge/http/remote-access.js +4 -4
  63. package/dist/edge/http/remote-guard.js +2 -2
  64. package/dist/edge/http/reviews.js +1 -1
  65. package/dist/edge/http/revision-files.js +1 -1
  66. package/dist/edge/http/revisions.js +4 -4
  67. package/dist/edge/http/schedules.js +13 -11
  68. package/dist/edge/http/settings.js +6 -2
  69. package/dist/edge/http/social.js +5 -4
  70. package/dist/edge/http/staging.js +1 -1
  71. package/dist/edge/http/storage.js +12 -4
  72. package/dist/edge/http/studio.js +71 -19
  73. package/dist/edge/http/style-preview.js +2 -2
  74. package/dist/edge/http/template-studio.js +12 -9
  75. package/dist/edge/http/trash.js +1 -1
  76. package/dist/edge/http/youtube-edits.js +1 -1
  77. package/dist/edge/mcp/excluded-routes.js +4 -3
  78. package/dist/edge/mcp/instructions.js +3 -3
  79. package/dist/edge/mcp/library-tools.js +8 -5
  80. package/dist/edge/mcp/remake-tools.js +7 -4
  81. package/dist/edge/mcp/server.js +1 -1
  82. package/dist/edge/mcp/tools-channels.js +9 -9
  83. package/dist/edge/mcp/tools-designs.js +9 -9
  84. package/dist/edge/mcp/tools-library.js +31 -7
  85. package/dist/edge/mcp/tools-planning.js +18 -18
  86. package/dist/edge/mcp/tools-projects.js +10 -10
  87. package/dist/edge/mcp/tools-publishing.js +9 -9
  88. package/dist/edge/mcp/tools-runs.js +9 -9
  89. package/dist/edge/mcp/tools-schedules.js +6 -6
  90. package/dist/edge/mcp/tools-setup.js +20 -14
  91. package/dist/edge/mcp/tools-social.js +5 -4
  92. package/dist/edge/mcp/tools-storage.js +3 -3
  93. package/dist/edge/mcp/tools-template-studio.js +18 -18
  94. package/dist/edge/remote-access/model.js +1 -1
  95. package/dist/extension/slopify-studio-chrome.zip +0 -0
  96. package/dist/extension/slopify-studio-firefox.zip +0 -0
  97. package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
  98. package/dist/kernel/pipeline.js +12 -0
  99. package/dist/kernel/ports/settings-places.js +31 -0
  100. package/dist/kernel/ports/system-speech.js +31 -3
  101. package/dist/kernel/runner/attempt.js +11 -3
  102. package/dist/kernel/runner/providers.js +3 -2
  103. package/dist/kernel/runner/standalone.js +2 -2
  104. package/dist/main.js +5 -3
  105. package/dist/model-catalog.js +20 -0
  106. package/dist/patch-notes/4.0.0.md +13 -13
  107. package/dist/patch-notes/4.1.0.md +42 -0
  108. package/dist/patch-notes/4.2.0.md +28 -0
  109. package/dist/patch-notes/index.json +12 -0
  110. package/dist/slices/admission/listing-facts.js +95 -0
  111. package/dist/slices/admission/repo.js +9 -6
  112. package/dist/slices/admission/rules.js +7 -7
  113. package/dist/slices/admission/schema.js +2 -0
  114. package/dist/slices/admission/start.js +7 -0
  115. package/dist/slices/admission/state-words.js +34 -5
  116. package/dist/slices/assistant/cost.js +1 -1
  117. package/dist/slices/assistant/gate.js +1 -0
  118. package/dist/slices/assistant/model.js +10 -3
  119. package/dist/slices/assistant/turn.js +5 -3
  120. package/dist/slices/autopilot/warnings.js +1 -1
  121. package/dist/slices/backups/service.js +5 -5
  122. package/dist/slices/channels/cast-images.js +1 -1
  123. package/dist/slices/channels/schema.js +1 -1
  124. package/dist/slices/channels/videos.js +1 -1
  125. package/dist/slices/control/providers.js +2 -2
  126. package/dist/slices/designs/budget.js +1 -1
  127. package/dist/slices/designs/calls.js +2 -2
  128. package/dist/slices/designs/check-context.js +4 -4
  129. package/dist/slices/designs/fields.js +4 -4
  130. package/dist/slices/designs/gate.js +7 -5
  131. package/dist/slices/designs/render-input.js +1 -1
  132. package/dist/slices/designs/requests.js +1 -1
  133. package/dist/slices/designs/runner.js +1 -1
  134. package/dist/slices/designs/sample.js +2 -1
  135. package/dist/slices/designs/service.js +1 -1
  136. package/dist/slices/designs/template-checks.js +2 -2
  137. package/dist/slices/designs/template-make.js +3 -3
  138. package/dist/slices/designs/templates.js +3 -3
  139. package/dist/slices/document/fonts.js +1 -1
  140. package/dist/slices/document/model.js +7 -4
  141. package/dist/slices/document/render.js +2 -1
  142. package/dist/slices/document/theme-schema.js +1 -1
  143. package/dist/slices/document/theme.js +49 -3
  144. package/dist/slices/episodes/summarize.js +2 -2
  145. package/dist/slices/estimate/index.js +28 -22
  146. package/dist/slices/estimate/requests.js +4 -4
  147. package/dist/slices/fixes/rules.js +3 -1
  148. package/dist/slices/fonts/catalog.js +2 -1
  149. package/dist/slices/fonts/coverage.js +2 -1
  150. package/dist/slices/images/appearance.js +2 -1
  151. package/dist/slices/images/scenes.js +2 -1
  152. package/dist/slices/keywords/digest.js +2 -1
  153. package/dist/slices/keywords/rules.js +2 -2
  154. package/dist/slices/keywords/specs.js +5 -5
  155. package/dist/slices/keywords/values.js +1 -1
  156. package/dist/slices/library/transfer.js +3 -3
  157. package/dist/slices/loudness/loudnorm.js +2 -1
  158. package/dist/slices/loudness/model.js +3 -2
  159. package/dist/slices/loudness/sentence-level.js +2 -1
  160. package/dist/slices/model-checks/rules.js +13 -0
  161. package/dist/slices/model-upkeep/model.js +1 -1
  162. package/dist/slices/model-upkeep/switch.js +3 -3
  163. package/dist/slices/model-upkeep/usage.js +13 -2
  164. package/dist/slices/narration/breaths.js +2 -1
  165. package/dist/slices/narration/clean-pieces.js +2 -1
  166. package/dist/slices/narration/pauses-model.js +3 -2
  167. package/dist/slices/narration/pronunciation-check.js +226 -0
  168. package/dist/slices/narration/pronunciation.js +7 -2
  169. package/dist/slices/narration/pronunciations-library.js +94 -0
  170. package/dist/slices/notifications/subject.js +2 -2
  171. package/dist/slices/onboarding/copy.js +19 -2
  172. package/dist/slices/onboarding/first-run.js +13 -38
  173. package/dist/slices/onboarding/install.js +2 -1
  174. package/dist/slices/onboarding/model.js +26 -21
  175. package/dist/slices/onboarding/preferred-model.js +14 -0
  176. package/dist/slices/onboarding/quick-short.js +103 -64
  177. package/dist/slices/onboarding/sample.js +4 -1
  178. package/dist/slices/onboarding/state.js +7 -0
  179. package/dist/slices/onboarding/template.js +6 -5
  180. package/dist/slices/outputs/add.js +3 -2
  181. package/dist/slices/outputs/service.js +2 -2
  182. package/dist/slices/patch-notes/library.js +1 -1
  183. package/dist/slices/play-drafts/readiness.js +3 -3
  184. package/dist/slices/play-drafts/review-inputs.js +1 -1
  185. package/dist/slices/play-drafts/service.js +17 -7
  186. package/dist/slices/project-templates/next-chapter.js +2 -1
  187. package/dist/slices/project-templates/provider-defaults.js +22 -0
  188. package/dist/slices/project-templates/setup-sections.js +26 -0
  189. package/dist/slices/project-templates/setup.js +6 -1
  190. package/dist/slices/rebuild/preview-details.js +5 -5
  191. package/dist/slices/rebuild/recipe-preparation.js +1 -1
  192. package/dist/slices/rebuild/recipe-save.js +2 -2
  193. package/dist/slices/rebuild/recipe-text.js +16 -4
  194. package/dist/slices/rebuild/recipe-validation.js +5 -4
  195. package/dist/slices/rebuild/recipe-voices.js +3 -2
  196. package/dist/slices/rebuild/recipe-work.js +4 -4
  197. package/dist/slices/rebuild/recovery.js +6 -6
  198. package/dist/slices/rebuild/remake-outdated.js +5 -5
  199. package/dist/slices/rebuild/review-redo.js +1 -1
  200. package/dist/slices/rebuild/runtime-animate.js +2 -2
  201. package/dist/slices/rebuild/runtime-article.js +4 -3
  202. package/dist/slices/rebuild/runtime-document.js +5 -4
  203. package/dist/slices/rebuild/runtime-export-bed.js +2 -1
  204. package/dist/slices/rebuild/runtime-export-edit.js +2 -1
  205. package/dist/slices/rebuild/runtime-export-inputs.js +5 -4
  206. package/dist/slices/rebuild/runtime-export-short.js +1 -1
  207. package/dist/slices/rebuild/runtime-export.js +4 -3
  208. package/dist/slices/rebuild/runtime-image.js +4 -3
  209. package/dist/slices/rebuild/runtime-local.js +3 -2
  210. package/dist/slices/rebuild/runtime-narration-text.js +3 -2
  211. package/dist/slices/rebuild/runtime-provider.js +7 -6
  212. package/dist/slices/rebuild/runtime-review.js +6 -5
  213. package/dist/slices/rebuild/runtime-shorts.js +6 -5
  214. package/dist/slices/rebuild/runtime-store.js +17 -2
  215. package/dist/slices/rebuild/runtime-subtitles.js +13 -12
  216. package/dist/slices/rebuild/runtime-voices.js +2 -1
  217. package/dist/slices/rebuild/runtime-youtube.js +3 -2
  218. package/dist/slices/rebuild/service-readiness.js +11 -3
  219. package/dist/slices/research/synthesis.js +3 -2
  220. package/dist/slices/reviews/external-model.js +1 -1
  221. package/dist/slices/reviews/holds.js +5 -4
  222. package/dist/slices/reviews/model.js +8 -5
  223. package/dist/slices/reviews/rules.js +3 -3
  224. package/dist/slices/reviews/verdict.js +2 -1
  225. package/dist/slices/revisions/image-plan.js +1 -1
  226. package/dist/slices/revisions/mutation-assets.js +2 -1
  227. package/dist/slices/revisions/mutations.js +3 -2
  228. package/dist/slices/schedules/agenda.js +7 -5
  229. package/dist/slices/schedules/design-check.js +4 -4
  230. package/dist/slices/schedules/design-run.js +3 -3
  231. package/dist/slices/schedules/repo.js +6 -5
  232. package/dist/slices/schedules/schema.js +3 -0
  233. package/dist/slices/schedules/topics.js +5 -5
  234. package/dist/slices/settings/first-run.js +6 -32
  235. package/dist/slices/settings/health.js +30 -15
  236. package/dist/slices/settings/key-test.js +1 -1
  237. package/dist/slices/settings/model.js +4 -2
  238. package/dist/slices/settings/playback.js +2 -2
  239. package/dist/slices/shorts/keep.js +2 -1
  240. package/dist/slices/shorts/pick.js +4 -3
  241. package/dist/slices/shorts/prompts.js +2 -1
  242. package/dist/slices/shorts/render.js +3 -2
  243. package/dist/slices/social/media.js +5 -4
  244. package/dist/slices/social/pack.js +1 -1
  245. package/dist/slices/social/post-prompt.js +2 -1
  246. package/dist/slices/social/reshape.js +2 -2
  247. package/dist/slices/social/serve.js +1 -1
  248. package/dist/slices/social/service.js +3 -2
  249. package/dist/slices/social/state.js +1 -1
  250. package/dist/slices/storage/backup-import.js +2 -2
  251. package/dist/slices/storage/portable.js +1 -1
  252. package/dist/slices/storage/trim.js +23 -13
  253. package/dist/slices/studio/calendar.js +3 -1
  254. package/dist/slices/studio/disclosure.js +4 -4
  255. package/dist/slices/studio/model.js +1 -1
  256. package/dist/slices/studio/pack.js +9 -8
  257. package/dist/slices/studio/plan.js +1 -1
  258. package/dist/slices/studio/settings.js +6 -6
  259. package/dist/slices/studio/videos.js +23 -4
  260. package/dist/slices/style-preview/render.js +3 -3
  261. package/dist/slices/style-preview/schema.js +1 -1
  262. package/dist/slices/subtitles/captions.js +1 -1
  263. package/dist/slices/template-studio/anatomy-view.js +46 -3
  264. package/dist/slices/template-studio/bakeoff.js +3 -3
  265. package/dist/slices/template-studio/budget.js +6 -3
  266. package/dist/slices/template-studio/cards.js +78 -0
  267. package/dist/slices/template-studio/checks.js +99 -50
  268. package/dist/slices/template-studio/for-all.js +5 -5
  269. package/dist/slices/template-studio/free-video.js +1 -1
  270. package/dist/slices/template-studio/free.js +5 -5
  271. package/dist/slices/template-studio/model.js +19 -15
  272. package/dist/slices/template-studio/pack-import.js +14 -14
  273. package/dist/slices/template-studio/pack.js +9 -7
  274. package/dist/slices/template-studio/paid.js +12 -5
  275. package/dist/slices/template-studio/quote.js +18 -11
  276. package/dist/slices/template-studio/ready.js +7 -7
  277. package/dist/slices/template-studio/runner.js +1 -1
  278. package/dist/slices/template-studio/save.js +12 -12
  279. package/dist/slices/template-studio/say-it-as.js +3 -3
  280. package/dist/slices/template-studio/schedule-gate.js +1 -1
  281. package/dist/slices/template-studio/schedule-price.js +3 -1
  282. package/dist/slices/template-studio/schedule-sample.js +5 -1
  283. package/dist/slices/template-studio/schema.js +25 -0
  284. package/dist/slices/template-studio/service.js +3 -1
  285. package/dist/slices/template-studio/status.js +26 -9
  286. package/dist/slices/template-studio/topic.js +3 -3
  287. package/dist/slices/thumbnail/by-llm.js +1 -1
  288. package/dist/slices/uploads/repo.js +37 -1
  289. package/dist/slices/uploads/rules.js +3 -2
  290. package/dist/slices/video/edit-list.js +2 -2
  291. package/dist/slices/video/edit-settings.js +1 -1
  292. package/dist/slices/video/encoding.js +14 -5
  293. package/dist/slices/video/ffmpeg.js +2 -2
  294. package/dist/slices/video/plan.js +2 -1
  295. package/dist/slices/video/slideshow.js +1 -1
  296. package/dist/slices/voices/attribution.js +3 -2
  297. package/dist/slices/voices/model.js +9 -8
  298. package/dist/slices/voices/script.js +2 -1
  299. package/dist/slices/youtube/answer.js +2 -1
  300. package/dist/slices/youtube/edits-repo.js +1 -1
  301. package/dist/tutorials/AI-CLIs.md +25 -25
  302. package/dist/tutorials/Agents-MCP.md +32 -32
  303. package/dist/tutorials/Assistant.md +5 -5
  304. package/dist/tutorials/Backups.md +7 -7
  305. package/dist/tutorials/Calendar.md +17 -25
  306. package/dist/tutorials/Cast-Library.md +19 -19
  307. package/dist/tutorials/Channels.md +40 -35
  308. package/dist/tutorials/Costs-and-Run-Cost.md +38 -40
  309. package/dist/tutorials/Create-Article.md +114 -0
  310. package/dist/tutorials/Create-Extras.md +112 -0
  311. package/dist/tutorials/Create-Images.md +165 -0
  312. package/dist/tutorials/Create-Narration.md +178 -0
  313. package/dist/tutorials/Create-Overview.md +244 -0
  314. package/dist/tutorials/Create-Video-and-Style.md +144 -0
  315. package/dist/tutorials/Docker.md +3 -3
  316. package/dist/tutorials/Document-Themes.md +32 -32
  317. package/dist/tutorials/Editing-a-Project.md +95 -94
  318. package/dist/tutorials/FAQ.md +18 -18
  319. package/dist/tutorials/First-Launch-and-Welcome.md +50 -64
  320. package/dist/tutorials/Home-and-Projects.md +101 -76
  321. package/dist/tutorials/Home.md +33 -33
  322. package/dist/tutorials/Install.md +13 -13
  323. package/dist/tutorials/Intros-and-Outros.md +33 -34
  324. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +62 -58
  325. package/dist/tutorials/Keywords-Slopify-Fills.md +21 -21
  326. package/dist/tutorials/Models.md +27 -27
  327. package/dist/tutorials/Multiple-Voices.md +41 -40
  328. package/dist/tutorials/Narration-Aliases-and-Glossary.md +101 -19
  329. package/dist/tutorials/Notifications.md +21 -17
  330. package/dist/tutorials/Other-Languages.md +15 -15
  331. package/dist/tutorials/PDF-Documents.md +38 -29
  332. package/dist/tutorials/Posting-to-Social-Sites.md +12 -12
  333. package/dist/tutorials/Project-Page.md +118 -120
  334. package/dist/tutorials/Prompts-and-Templates.md +84 -0
  335. package/dist/tutorials/Prompts.md +45 -45
  336. package/dist/tutorials/Providers-and-Keys.md +105 -98
  337. package/dist/tutorials/Publishing-to-YouTube.md +31 -31
  338. package/dist/tutorials/Recovery-and-Retries.md +31 -31
  339. package/dist/tutorials/Remote-Access.md +2 -2
  340. package/dist/tutorials/Reviews-and-Checkpoints.md +90 -93
  341. package/dist/tutorials/Schedules.md +27 -28
  342. package/dist/tutorials/Settings-Reference.md +183 -204
  343. package/dist/tutorials/Shorts.md +34 -33
  344. package/dist/tutorials/Start-at-Login.md +3 -3
  345. package/dist/tutorials/Studio-Extension.md +22 -24
  346. package/dist/tutorials/Template-Studio.md +45 -42
  347. package/dist/tutorials/Templates.md +50 -79
  348. package/dist/tutorials/Trash.md +4 -4
  349. package/dist/tutorials/Troubleshooting.md +44 -44
  350. package/dist/tutorials/Uninstalling-and-Moving.md +5 -5
  351. package/dist/tutorials/Updating-and-Patch-Notes.md +18 -18
  352. package/dist/tutorials/Video-Editing.md +34 -34
  353. package/dist/tutorials/Where-Your-Files-Live.md +10 -10
  354. package/dist/tutorials/YouTube-Description.md +26 -26
  355. package/dist/tutorials/Your-First-Short.md +35 -35
  356. package/dist/tutorials/_Sidebar.md +15 -15
  357. package/dist/web/assets/index-BNftKQrA.css +1 -0
  358. package/dist/web/assets/index-CQASqwjT.js +460 -0
  359. package/dist/web/assets/pdf-BKh8f7R6.js +56 -0
  360. package/dist/web/assets/pdf.worker.min-BmVo14Nb.mjs +28 -0
  361. package/dist/web/assets/pdf.worker.min-D7i8o3wR.js +1 -0
  362. package/dist/web/index.html +2 -2
  363. package/package.json +2 -2
  364. package/dist/tutorials/Library-Overview.md +0 -76
  365. package/dist/tutorials/Play-Images.md +0 -165
  366. package/dist/tutorials/Play-Narration.md +0 -167
  367. package/dist/tutorials/Play-Outputs.md +0 -109
  368. package/dist/tutorials/Play-Overview.md +0 -227
  369. package/dist/tutorials/Play-Title-and-Article.md +0 -136
  370. package/dist/tutorials/Play-Video-and-Style.md +0 -143
  371. package/dist/web/assets/index-CkAI_2E7.css +0 -1
  372. package/dist/web/assets/index-DcD0cdfo.js +0 -334
  373. package/dist/web/assets/pdf-TODRwF31.js +0 -54
  374. package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +0 -29
  375. package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +0 -1
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { jsonBetween } from "../../kernel/json-answer.js";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  export function imagePromptMessages(brief) {
4
5
  const plural = brief.count === 1 ? "prompt" : "prompts";
5
6
  return [
@@ -32,7 +33,7 @@ export function imagePromptMessages(brief) {
32
33
  },
33
34
  ];
34
35
  }
35
- const fix = "Try again, or choose another model in Edit project → Providers.";
36
+ const fix = `Try again, or choose another model in ${editSettings.writer}.`;
36
37
  // The reason is the sentence the stage shows; the provider wrapper's `check` asks the model
37
38
  // again while attempts remain.
38
39
  export function checkImagePrompts(text, count) {
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { editListVersion, } from "../video/edit-list.js";
4
5
  import { runFfmpeg } from "../video/ffmpeg.js";
5
6
  import { motionFor } from "../video/motion.js";
@@ -46,7 +47,7 @@ export function shortAudioArgs(timeline, start, end, output, options = {}) {
46
47
  count += 1;
47
48
  }
48
49
  if (count === 0 || first === undefined)
49
- throw new Error("A short's clip falls outside the narration, so it has no sound to cut. Use Pick different moments in Edit project → Shorts, then Try again.");
50
+ throw new Error(`A short's clip falls outside the narration, so it has no sound to cut. Use Pick different moments in ${editSettings.shorts}, then Try again.`);
50
51
  const joined = Array.from({ length: count }, (_value, at) => `[a${String(at)}]`).join("");
51
52
  chains.push(`${joined}concat=n=${String(count)}:v=0:a=1[joined]`);
52
53
  const speed = options.speed ?? 1;
@@ -89,7 +90,7 @@ export function shortAudioArgs(timeline, start, end, output, options = {}) {
89
90
  // moving as the video's shots move by their place.
90
91
  export function shortEditList(input) {
91
92
  if (input.images.length === 0)
92
- throw new Error("A short has no images to show. Use Make this short again in Edit project → Shorts, then Try again.");
93
+ throw new Error(`A short has no images to show. Use Make this short again in ${editSettings.shorts}, then Try again.`);
93
94
  const total = Math.max(1, Math.round(input.seconds * fps));
94
95
  const each = Math.max(1, Math.round(input.imageSeconds * fps));
95
96
  const figures = figureFrames(input.figures ?? [], total);
@@ -1,6 +1,7 @@
1
1
  // Which files a post carries, in the shape its site wants them. Pure: the sources come from
2
2
  // the project's outputs (`sources.ts`); a reshaped picture or a design's render is made the
3
3
  // first time it is asked for (`reshape.ts`), so listing a pack never draws anything.
4
+ import { editSettings } from "../../kernel/ports/settings-places.js";
4
5
  import { limitsFor } from "./limits.js";
5
6
  import { platformLabels, } from "./model.js";
6
7
  const shapeSlugs = {
@@ -65,7 +66,7 @@ export function mediaFor(platform, choice, sources) {
65
66
  const shape = limitsFor(platform).pictureShape;
66
67
  if (choice.video === "long") {
67
68
  if (sources.long === null)
68
- missing("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open Prepare upload again.");
69
+ missing("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open Copy for YouTube Studio again.");
69
70
  else
70
71
  media.push({
71
72
  kind: "video",
@@ -83,8 +84,8 @@ export function mediaFor(platform, choice, sources) {
83
84
  const short = sources.shorts.find((one) => one.number === number);
84
85
  if (short?.file == null)
85
86
  missing(short === undefined
86
- ? `This project has no short ${String(number)}. Pick another short for ${site} in the post's media, or turn on Shorts in Edit project.`
87
- : `Short ${String(number)} isn't rendered yet. Let the Video stage finish, then open Prepare upload again.`);
87
+ ? `This project has no short ${String(number)}. Pick another short for ${site} in the post's media, or turn on Shorts in ${editSettings.shorts}.`
88
+ : `Short ${String(number)} isn't rendered yet. Let the Video stage finish, then open Copy for YouTube Studio again.`);
88
89
  else
89
90
  media.push({
90
91
  kind: "short",
@@ -99,7 +100,7 @@ export function mediaFor(platform, choice, sources) {
99
100
  }
100
101
  if (choice.thumbnail === true) {
101
102
  if (sources.thumbnail === null)
102
- missing("The thumbnail isn't made yet. Let the Thumbnail stage finish, or set Thumbnail in Edit project.");
103
+ missing(`The thumbnail isn't made yet. Let the Thumbnail stage finish, or set Thumbnail source in ${editSettings.extras}.`);
103
104
  else
104
105
  media.push(still(sources, "thumbnail", sources.thumbnail, shape));
105
106
  }
@@ -94,7 +94,7 @@ function buildPack(sources, platform, record, now) {
94
94
  if (choice.link && link === null)
95
95
  warnings.push({
96
96
  code: "link-missing",
97
- sentence: "The YouTube link isn't known yet, so the post goes without it. It is added once the video is on YouTube (Prepare upload → Fill in YouTube Studio, or paste the video's link there).",
97
+ sentence: "The YouTube link isn't known yet, so the post goes without it. It is added once the video is on YouTube (Copy for YouTube Studio → Fill in YouTube Studio, or paste the video's link there).",
98
98
  });
99
99
  const hashtags = hashtagsFor([sources.tags, sources.shortHashtags], Math.min(choice.hashtags, limits.hashtags));
100
100
  const fitted = fitText({ body, link, hashtags }, limits, site);
@@ -1,5 +1,6 @@
1
1
  // The request that writes a post for another site with a Library prompt of kind "post" (or the
2
2
  // built-in one), and the check its answer must pass. Pure.
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { limitsFor } from "./limits.js";
4
5
  import { platformLabels } from "./model.js";
5
6
  import { countFor } from "./text.js";
@@ -45,7 +46,7 @@ export function postText(answer) {
45
46
  export function checkPost(answer, target) {
46
47
  const site = platformLabels[target.platform];
47
48
  const text = postText(answer);
48
- const fix = "Use Try again; if it keeps happening, shorten what the post prompt asks for in Library → Prompts → Post, or choose another model in Edit project → Providers.";
49
+ const fix = `Use Try again; if it keeps happening, shorten what the post prompt asks for in Prompts → Post, or choose another model in ${editSettings.writer}.`;
49
50
  if (text === "")
50
51
  return `The AI model sent back an empty ${site} post. ${fix}`;
51
52
  const used = countFor(text, target.unit);
@@ -46,7 +46,7 @@ export function createReshaper(deps) {
46
46
  if (stat === undefined || !stat.isFile())
47
47
  return {
48
48
  ok: false,
49
- sentence: "The picture isn't on disk any more. Reload Prepare upload; if it is still missing, make the project's images again.",
49
+ sentence: "The picture isn't on disk any more. Close Copy for YouTube Studio and open it again; if it is still missing, make the project's images again.",
50
50
  };
51
51
  const key = createHash("sha256")
52
52
  .update(JSON.stringify([reshapeVersion, source, stat.size, stat.mtimeMs, shape]))
@@ -82,7 +82,7 @@ export function createReshaper(deps) {
82
82
  });
83
83
  return {
84
84
  ok: false,
85
- sentence: `The picture couldn't be cut to ${shape} because ffmpeg stopped with an error. Reload Prepare upload to try again; if it fails again, use Download diagnostics in Settings and report it.`,
85
+ sentence: `The picture couldn't be cut to ${shape} because ffmpeg stopped with an error. Close Copy for YouTube Studio and open it again to try again; if it fails again, use Download diagnostics in Settings and report it.`,
86
86
  };
87
87
  }
88
88
  finally {
@@ -2,7 +2,7 @@ import { statSync } from "node:fs";
2
2
  import { findDownload } from "../storage/downloads.js";
3
3
  import { parseMediaFile } from "./media.js";
4
4
  import { postPacks } from "./pack.js";
5
- const gone = "That file isn't in the project's posts any more. Reload Prepare upload and press Fill again.";
5
+ const gone = "That file isn't in the project's posts any more. Close Copy for YouTube Studio, open it again and press Fill again.";
6
6
  // One file of a project's posts, only when one of its posts lists it: the asset as it is, a
7
7
  // picture cut to its site's shape (made now the first time), or a design drawn in its format.
8
8
  export async function serveMedia(deps, projectId, file, now, signal) {
@@ -1,5 +1,6 @@
1
1
  import { projectById } from "../admission/repo.js";
2
2
  import { projectChannelId } from "../channels/repo.js";
3
+ import { setupSectionNames } from "../project-templates/setup-sections.js";
3
4
  import { channelSocial } from "./channel.js";
4
5
  import { dueAt, readyNow, tooLate, waitsForRelease } from "./due.js";
5
6
  import { profileUrl } from "./limits.js";
@@ -9,10 +10,10 @@ import { packFor } from "./pack.js";
9
10
  import { expireWaiting, takeNext } from "./queue.js";
10
11
  import { postsIn, readPost, writeProgress } from "./repo.js";
11
12
  import { askFill, pasteLink, reportFill } from "./state.js";
12
- // Edit project has no posting editor: a project's sites come from its template (or Play).
13
+ // A project's settings have no posting editor: its sites come from its template (or Create).
13
14
  export function notPostingSentence(platform) {
14
15
  const site = platformLabels[platform];
15
- return `This project doesn't post to ${site}. A project's sites come from its template: turn ${site} on under Post to in Template Studio → Upload pack (Library → Templates → Open studio), or on Play under Outputs, for the projects it makes next.`;
16
+ return `This project doesn't post to ${site}. A project's sites come from its template: tick ${site} under Post to in Edit template → ${setupSectionNames.youtube}, or under Post to other sites in Create, for the projects it makes next.`;
16
17
  }
17
18
  const notPosting = (platform) => ({
18
19
  ok: false,
@@ -31,7 +31,7 @@ export function reportFill(current, platform, report, now) {
31
31
  return {
32
32
  ok: false,
33
33
  reason: "not-asked",
34
- sentence: `Nothing was waiting to be filled in ${site} for this project. In Slopify, open the project, press Prepare upload, then Fill in ${site}.`,
34
+ sentence: `Nothing was waiting to be filled in ${site} for this project. In Slopify, open the project, press Copy for YouTube Studio, then Fill in ${site}.`,
35
35
  };
36
36
  const url = report.url === undefined || report.url === "" ? null : report.url;
37
37
  if (url !== null && !linkPatterns[platform].test(url))
@@ -436,7 +436,7 @@ function checkScratch(deps, scratch, library, parts) {
436
436
  }
437
437
  }
438
438
  catch {
439
- throw unreadable("a PDF theme");
439
+ throw unreadable("a PDF look");
440
440
  }
441
441
  try {
442
442
  for (const row of rowsOf(scratch, "SELECT id FROM channels")) {
@@ -465,7 +465,7 @@ function checkScratch(deps, scratch, library, parts) {
465
465
  throw new Error("staged files");
466
466
  }
467
467
  catch {
468
- throw unreadable("a Play draft");
468
+ throw unreadable("a draft on Create");
469
469
  }
470
470
  try {
471
471
  allTelemetryEvents(scratch);
@@ -362,7 +362,7 @@ const storedAppearance = z.enum(appearances);
362
362
  const storedLoudness = z
363
363
  .object({ enabled: z.boolean(), videoLufs: z.number(), audioFilesLufs: z.number() })
364
364
  .strict();
365
- // Settings → Channel links, and the providers a fresh Play form starts with.
365
+ // Settings → YouTube Studio and posting, and the providers a fresh Play form starts with.
366
366
  // The AI use marks: ticked voices, Image prompts and projects, by id.
367
367
  const storedIds = z.array(z.string().max(200)).max(100_000);
368
368
  // The channel page's "Keep only titles containing…" text, one per channel.
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, rmdirSync, statSync, unlinkSync } from "node:f
2
2
  import { join, relative, sep } from "node:path";
3
3
  import { derive } from "../../kernel/runner/graph.js";
4
4
  import { liveProject, projectExists, projectPaused, stagesOf } from "../admission/repo.js";
5
+ import { bundledSampleProjects } from "../onboarding/state.js";
5
6
  import { notTemplateSample } from "../template-studio/repo.js";
6
7
  import { projectSettled } from "./arrange.js";
7
8
  import { backupsFolderName, outputPath, projectDir } from "./layout.js";
@@ -11,7 +12,9 @@ import { pieceFile } from "./reconcile.js";
11
12
  // video and shorts show included: making them again costs money and never gives the same ones. Two
12
13
  // rules keep it safe. A file the user supplied is never removed, because nothing could make
13
14
  // it again. And only files a project record names are removed: anything else in the folder
14
- // is reconcile's business (`reconcile.ts`), which leaves the Backups folder alone.
15
+ // is reconcile's business (`reconcile.ts`), which leaves the Backups folder alone. A bundled
16
+ // sample is read-only and keeps every file: nothing on it counts as removable, and no clean-up
17
+ // touches it.
15
18
  // What a finished project is for: the files that get uploaded or read, from every revision,
16
19
  // so an older video in History stays too.
17
20
  export const outputRoles = [
@@ -37,14 +40,16 @@ export const outputRoles = [
37
40
  ];
38
41
  export function projectStorage(deps, projectId) {
39
42
  const files = filesOf(deps.paths, projectId);
40
- const kept = keptPaths(deps.db, projectId);
41
43
  const outputs = outputPaths(deps.db, projectId);
42
- const removable = removablePaths(deps.db, projectId, kept);
44
+ const sample = bundledSampleProjects(deps.db).has(projectId);
45
+ const removable = sample
46
+ ? new Set()
47
+ : removablePaths(deps.db, projectId, keptPaths(deps.db, projectId));
43
48
  let outputsBytes = 0;
44
49
  let workingBytes = 0;
45
50
  let removableFiles = 0;
46
51
  let removableBytes = 0;
47
- const history = historyPaths(deps, projectId);
52
+ const history = sample ? new Set() : historyPaths(deps, projectId);
48
53
  let historyFiles = 0;
49
54
  let historyBytes = 0;
50
55
  for (const [path, bytes] of files) {
@@ -108,17 +113,15 @@ export function deleteOldVersions(deps, projectId) {
108
113
  return { ok: true, files: count, bytesFreed };
109
114
  }
110
115
  // Every project's old versions, skipping any that runs or waits to run. A template studio
111
- // sample keeps its files (its bake-offs and resamples reuse them), and is no project to count.
116
+ // sample keeps its files (its bake-offs and resamples reuse them), and is no project to count;
117
+ // nor is a bundled sample, which is read-only.
112
118
  export function deleteAllOldVersions(deps) {
113
119
  const busyIds = [];
114
120
  let projects = 0;
115
121
  let files = 0;
116
122
  let bytesFreed = 0;
117
123
  let busy = 0;
118
- for (const row of deps.db
119
- .prepare(`SELECT id FROM projects WHERE ${liveProject()} AND ${notTemplateSample()}`)
120
- .all()) {
121
- const id = String(row.id);
124
+ for (const id of cleanable(deps.db)) {
122
125
  if (placedFiles(deps.paths, id).size === 0)
123
126
  continue;
124
127
  const result = deleteOldVersions(deps, id);
@@ -143,10 +146,7 @@ export function cleanUpAll(deps) {
143
146
  let files = old.files;
144
147
  let bytesFreed = old.bytesFreed;
145
148
  let skipped = old.busy;
146
- for (const row of deps.db
147
- .prepare(`SELECT id FROM projects WHERE ${liveProject()} AND ${notTemplateSample()}`)
148
- .all()) {
149
- const id = String(row.id);
149
+ for (const id of cleanable(deps.db)) {
150
150
  const result = keepOutputsOnly(deps, id);
151
151
  if (result.ok) {
152
152
  files += result.files;
@@ -157,6 +157,16 @@ export function cleanUpAll(deps) {
157
157
  }
158
158
  return { files, bytesFreed, skipped };
159
159
  }
160
+ // The projects the clean-ups for every project go through: neither a template studio sample nor
161
+ // a bundled one.
162
+ function cleanable(db) {
163
+ const samples = bundledSampleProjects(db);
164
+ return db
165
+ .prepare(`SELECT id FROM projects WHERE ${liveProject()} AND ${notTemplateSample()}`)
166
+ .all()
167
+ .map((row) => String(row.id))
168
+ .filter((id) => !samples.has(id));
169
+ }
160
170
  // The folders under `root` left empty, deepest first, and `root` itself if it is empty then.
161
171
  function removeEmptyUnder(root) {
162
172
  if (!existsSync(root))
@@ -1,4 +1,5 @@
1
1
  import { listProjects } from "../admission/repo.js";
2
+ import { uploadedProjects } from "../uploads/repo.js";
2
3
  import { uploadPack } from "./pack.js";
3
4
  import { readPlan } from "./plan.js";
4
5
  import { seriesOf } from "./plan-model.js";
@@ -16,6 +17,7 @@ export function releaseCalendar(deps, options) {
16
17
  return at >= from.getTime() && at < until.getTime();
17
18
  };
18
19
  const projects = new Map(listProjects(deps.db).map((project) => [project.id, project]));
20
+ const uploaded = uploadedProjects(deps.db);
19
21
  const longs = allReleases(deps.db).filter((release) => release.short === 0 && projects.has(release.projectId) && inWindow(release.at));
20
22
  // A schedule's line shows the schedule's name; a stored line its series.
21
23
  const seriesOfLine = (line) => {
@@ -39,7 +41,7 @@ export function releaseCalendar(deps, options) {
39
41
  const release = at === undefined || at === "" ? null : at;
40
42
  const uploadBy = release === null ? null : new Date(Date.parse(release) - leadMs);
41
43
  const video = videoOf(deps.db, long.projectId, item.short ?? null);
42
- const state = video?.uploadState === "done"
44
+ const state = video?.uploadState === "done" || (short === 0 && uploaded.has(long.projectId))
43
45
  ? "scheduled"
44
46
  : video?.uploadState === "filled"
45
47
  ? "filled"
@@ -16,7 +16,7 @@
16
16
  // So the automatic answer is Yes only when one of the three cases applies, and No otherwise:
17
17
  // 1. a saved voice marked "Imitates a real person" (Settings → Voices) narrates it: an AI
18
18
  // voice cloned from, or made to sound like, a real person, so that person seems to say it;
19
- // 2. uploaded clips the person marked as real footage (the project's Prepare upload) get the
19
+ // 2. uploaded clips the person marked as real footage (the project's Copy for YouTube Studio) get the
20
20
  // Look's atmosphere overlay (embers, dust or fog), which adds to a real scene what wasn't
21
21
  // there. A colour grade, vignette or grain alone is one of YouTube's minor edits;
22
22
  // 3. an AI image model draws its pictures from an Image prompt marked "Draws photorealistic
@@ -49,18 +49,18 @@ export function aiDisclosureOf(input) {
49
49
  if (input.realFootage && input.footageOverlay !== undefined)
50
50
  reasons.push(`the Look's ${input.footageOverlay} overlay is laid over uploaded clips marked as real footage (YouTube's case 2: "${aiUseCases[1]}")`);
51
51
  if (input.photorealistic)
52
- reasons.push(`an AI image model draws its pictures from an Image prompt marked photorealistic in Library → Prompts (YouTube's case 3: "${aiUseCases[2]}")`);
52
+ reasons.push(`an AI image model draws its pictures from an Image prompt marked photorealistic in Prompts (YouTube's case 3: "${aiUseCases[2]}")`);
53
53
  if (reasons.length > 0)
54
54
  return {
55
55
  altered: true,
56
56
  why: `Yes because ${list(reasons)}. Selecting Yes adds YouTube's AI label.`,
57
57
  };
58
58
  const footage = input.realFootage && input.kind === "video"
59
- ? " The uploaded real footage only gets colour, vignette or grain, which YouTube counts as minor edits."
59
+ ? " The uploaded real footage only gets color, vignette or grain, which YouTube counts as minor edits."
60
60
  : "";
61
61
  return {
62
62
  altered: false,
63
- why: `No because none of YouTube's three AI use cases applies: no narrating voice is marked as imitating a real person (Settings → Voices), no real footage is altered, and no AI pictures come from an Image prompt marked photorealistic (Library → Prompts). Stylised AI pictures and an AI narrator that doesn't pose as a real person don't need the label.${footage}`,
63
+ why: `No because none of YouTube's three AI use cases applies: no narrating voice is marked as imitating a real person (Settings → Voices), no real footage is altered, and no AI pictures come from an Image prompt marked photorealistic (Prompts). Stylized AI pictures and an AI narrator that doesn't pose as a real person don't need the label.${footage}`,
64
64
  };
65
65
  }
66
66
  function quotedList(names) {
@@ -13,7 +13,7 @@ export const studioAudience = "not_made_for_kids";
13
13
  export const studioUploadUrl = "https://www.youtube.com/upload";
14
14
  // What a playlist takes: every upload, only long videos, or only shorts.
15
15
  export const playlistUses = ["all", "long", "shorts"];
16
- // The steps in Studio's order, as the Prepare upload view lists them.
16
+ // The steps in Studio's order, as Copy for YouTube Studio lists them.
17
17
  export const studioSteps = [
18
18
  "video",
19
19
  "title",
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { z } from "zod";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { thumbnailCountOf } from "../admission/model.js";
4
5
  import { projectById } from "../admission/repo.js";
5
6
  import { channelById, projectChannelId } from "../channels/repo.js";
@@ -55,7 +56,7 @@ export function uploadPack(deps, projectId) {
55
56
  ? undefined
56
57
  : readFileSync(outputPath(deps.paths, projectId, output.path), "utf8").trim();
57
58
  const channelId = projectChannelId(deps.db, projectId);
58
- // The channel's playlists (Settings → YouTube Studio), ticked as this project chose.
59
+ // The channel's playlists (Settings → YouTube Studio and posting), ticked as this project chose.
59
60
  const playlistChoices = projectPlaylists(deps.db, projectId, channelId);
60
61
  // The long video goes into the chosen playlists that take long videos, each short into those
61
62
  // that take shorts.
@@ -88,7 +89,7 @@ export function uploadPack(deps, projectId) {
88
89
  });
89
90
  const video = outputs.find((output) => output.role === "video");
90
91
  if (video === undefined)
91
- missing.push("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open Prepare upload again.");
92
+ missing.push("The video isn't made yet. Let the Video stage finish (or use Continue the run on the project page), then open Copy for YouTube Studio again.");
92
93
  const written = text(outputs.find((output) => output.role === "youtube_description"));
93
94
  const writtenTags = text(outputs.find((output) => output.role === "youtube_tags"));
94
95
  // As the project page shows it: the user's edits, and channel links filled in.
@@ -109,7 +110,7 @@ export function uploadPack(deps, projectId) {
109
110
  if (description === undefined)
110
111
  missing.push(config.youtubeDescription === true
111
112
  ? "The YouTube description isn't written yet. It is written after the video's subtitle timing; wait for the Video stage to finish."
112
- : "No YouTube description is written for this project. Turn on YouTube description in Edit project → Prompts to have one written with chapters and tags.");
113
+ : `No YouTube description is written for this project. Turn on YouTube description in ${editSettings.extras} to have one written with chapters and tags.`);
113
114
  const count = thumbnailCountOf(config);
114
115
  // The first thumbnail, then the second and third, and never one left from when the project
115
116
  // made three and now makes one.
@@ -118,12 +119,12 @@ export function uploadPack(deps, projectId) {
118
119
  .sort((a, b) => (a.meta.index ?? 1) - (b.meta.index ?? 1));
119
120
  if (thumbnails.length === 0)
120
121
  missing.push(config.sources.thumbnail === "off"
121
- ? "This project makes no thumbnail, so YouTube will pick a frame. Set Thumbnail in Edit project to make or upload one."
122
- : "The thumbnail isn't made yet. Let the Thumbnail stage finish, then open Prepare upload again.");
122
+ ? `This project makes no thumbnail, so YouTube will pick a frame. Set Thumbnail source in ${editSettings.extras} to make or upload one.`
123
+ : "The thumbnail isn't made yet. Let the Thumbnail stage finish, then open Copy for YouTube Studio again.");
123
124
  else if (thumbnails.length < count)
124
- missing.push(`Only ${String(thumbnails.length)} of the ${String(count)} thumbnails are made. Regenerate the missing ones in the project's Images section.`);
125
+ missing.push(`Only ${String(thumbnails.length)} of the ${String(count)} thumbnails are made. Make the missing ones in the project's Images section.`);
125
126
  if (playlistChoices.length === 0)
126
- missing.push("No playlist is set for this project's channel, so the playlist step is left to you. Set one in Settings → YouTube Studio → Playlists.");
127
+ missing.push("No playlist is set for this project's channel, so the playlist step is left to you. Set one in Settings → YouTube Studio and posting → Playlists.");
127
128
  // The video's titles and thumbnails in the project's order, and the ones the person picked
128
129
  // for the upload first (`pick.ts`); the A/B test later tries the rest beside them.
129
130
  const ownTitle = project.title.slice(0, studioTitleMax);
@@ -189,7 +190,7 @@ export function uploadPack(deps, projectId) {
189
190
  output.meta.sentences?.[1] === clip.last);
190
191
  const render = exact ?? mine.at(-1);
191
192
  if (render === undefined)
192
- missing.push(`Short ${String(clip.number)} isn't rendered yet. Let the Video stage finish, then open Prepare upload again.`);
193
+ missing.push(`Short ${String(clip.number)} isn't rendered yet. Let the Video stage finish, then open Copy for YouTube Studio again.`);
193
194
  items.push({
194
195
  kind: "short",
195
196
  short: clip.number,
@@ -11,7 +11,7 @@ export { postingPlanSchema, weekdays, } from "./plan-model.js";
11
11
  import { postingPlanSchema, seriesOf } from "./plan-model.js";
12
12
  export const postingPlanKey = "studio.postingPlan";
13
13
  const key = postingPlanKey;
14
- // No plan until one is made: nothing is scheduled, and Prepare upload says so.
14
+ // No plan until one is made: nothing is scheduled, and Copy for YouTube Studio says so.
15
15
  export function emptyPlan(timeZone) {
16
16
  return { timeZone, rows: [] };
17
17
  }
@@ -14,7 +14,7 @@ const pairingSchema = z.object({
14
14
  });
15
15
  // A channel's own playlists are a row of their own, `studio.playlist.<channelId>`; a channel
16
16
  // without one uses the default list above. Each playlist is ticked by default or not; a
17
- // project's Prepare upload can change which it goes into. A row saved before lists existed
17
+ // project's Copy for YouTube Studio can change which it goes into. A row saved before lists existed
18
18
  // holds one name, read as one playlist ticked by default.
19
19
  export const studioChannelPlaylistPrefix = `${studioPlaylistKey}.`;
20
20
  function playlistKey(channelId) {
@@ -65,16 +65,16 @@ function storedPlaylists(db, key) {
65
65
  return stored.trim() === "" ? null : [{ name: stored.trim(), byDefault: true }];
66
66
  return stored.length === 0 ? null : stored;
67
67
  }
68
- // Why a list can't be saved, in words for Settings → YouTube Studio; undefined when it can.
68
+ // Why a list can't be saved, in words for Settings → YouTube Studio and posting; undefined when it can.
69
69
  export function studioPlaylistsProblem(list) {
70
70
  const long = list.find((one) => one.name.trim().length > studioPlaylistMax);
71
71
  if (long !== undefined)
72
- return `The playlist name "${long.name.slice(0, 40)}…" is longer than YouTube allows (${String(studioPlaylistMax)} characters). Shorten it in Settings → YouTube Studio.`;
72
+ return `The playlist name "${long.name.slice(0, 40)}…" is longer than YouTube allows (${String(studioPlaylistMax)} characters). Shorten it in Settings → YouTube Studio and posting.`;
73
73
  const seen = new Set();
74
74
  for (const one of list) {
75
75
  const key = one.name.trim().toLowerCase();
76
76
  if (seen.has(key))
77
- return `"${one.name.trim()}" is listed twice. Remove one in Settings → YouTube Studio.`;
77
+ return `"${one.name.trim()}" is listed twice. Remove one in Settings → YouTube Studio and posting.`;
78
78
  seen.add(key);
79
79
  }
80
80
  return undefined;
@@ -98,7 +98,7 @@ export function saveStudioPlaylists(db, list, channelId) {
98
98
  writeSetting(db, key, JSON.stringify(value));
99
99
  return value;
100
100
  }
101
- // The playlists a project's uploads go into, when its Prepare upload changed them from the
101
+ // The playlists a project's uploads go into, when its Copy for YouTube Studio changed them from the
102
102
  // channel's defaults: one row, project id to names.
103
103
  export const studioProjectPlaylistsKey = "studio.projectPlaylists";
104
104
  export const storedProjectPlaylistsSchema = z.record(z.string().max(64), z.array(z.string().max(studioPlaylistMax)).max(studioPlaylistsMax));
@@ -127,7 +127,7 @@ export function saveProjectPlaylists(db, projectId, names) {
127
127
  writeSetting(db, studioProjectPlaylistsKey, JSON.stringify(next));
128
128
  }
129
129
  // The projects whose uploaded clips are real footage (filmed, not generated): ticked in the
130
- // project's Prepare upload, under AI use. One row holding their ids; YouTube's second AI use
130
+ // project's Copy for YouTube Studio, under AI use. One row holding their ids; YouTube's second AI use
131
131
  // case is footage of a real event or place altered by AI (`disclosure.ts`).
132
132
  export const studioRealFootageKey = "studio.realFootage";
133
133
  // "on" or "off": whether the extension posts and pins each video's comment once it is public.
@@ -1,3 +1,4 @@
1
+ import { markUploaded } from "../uploads/repo.js";
1
2
  const taskState = (value) => value === "waiting" || value === "done" || value === "failed" ? value : "none";
2
3
  // A YouTube video id: 11 letters, digits, `-` and `_`.
3
4
  export const videoIdPattern = /^[A-Za-z0-9_-]{11}$/;
@@ -41,15 +42,33 @@ export function recordVideo(db, projectId, short, videoId, at, state = "done") {
41
42
  upload_state=CASE WHEN video_id=excluded.video_id AND upload_state='done' THEN 'done'
42
43
  ELSE excluded.upload_state END,
43
44
  video_id=excluded.video_id, recorded_at=excluded.recorded_at`).run(projectId, slot(short), videoId, at, state);
45
+ if (short === null && videoOf(db, projectId, null)?.uploadState === "done")
46
+ markUploaded(db, projectId, true, at);
44
47
  }
45
48
  // Studio said the upload was scheduled or published. False when no such upload was filled.
46
- export function confirmUpload(db, projectId, short, videoId) {
47
- return (db
49
+ export function confirmUpload(db, projectId, short, videoId, at) {
50
+ const confirmed = db
48
51
  .prepare("UPDATE youtube_videos SET upload_state='done' WHERE project_id=? AND short=? AND video_id=?")
49
- .run(projectId, slot(short), videoId).changes > 0);
52
+ .run(projectId, slot(short), videoId).changes > 0;
53
+ if (confirmed && short === null)
54
+ markUploaded(db, projectId, true, at);
55
+ return confirmed;
50
56
  }
51
- export function forgetVideo(db, projectId, short) {
57
+ // Forgets the video an upload became (deleted on YouTube, or a wrong link). Forgetting a long
58
+ // video that was on YouTube takes the project's uploaded mark off with it; an upload only
59
+ // filled in Studio never set the mark, so forgetting it leaves a mark made by hand.
60
+ export function forgetVideo(db, projectId, short, at) {
61
+ const was = videoOf(db, projectId, short);
52
62
  db.prepare("DELETE FROM youtube_videos WHERE project_id=? AND short=?").run(projectId, slot(short));
63
+ if (short === null && was?.uploadState === "done")
64
+ markUploaded(db, projectId, false, at);
65
+ }
66
+ // "Mark not uploaded" (Home, Projects): the long video's link goes with the mark, so the two
67
+ // never disagree. An upload only filled in Studio is not on YouTube and stays.
68
+ export function markNotUploaded(db, projectId, at) {
69
+ if (videoOf(db, projectId, null)?.uploadState === "done")
70
+ forgetVideo(db, projectId, null, at);
71
+ return markUploaded(db, projectId, false, at);
53
72
  }
54
73
  export function projectVideos(db, projectId) {
55
74
  return db
@@ -162,9 +162,9 @@ function explained(log, error) {
162
162
  return error;
163
163
  const message = errorText(error);
164
164
  if (isMissingFont(error))
165
- return new StylePreviewError("font-missing", "The style preview couldn't render because the chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another font under Subtitles, then press Render again.");
165
+ return new StylePreviewError("font-missing", "The style preview couldn't render because the chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another caption font, then press Make the preview again.");
166
166
  if (message.startsWith("Slopify could not start ffmpeg") || /\bENOENT\b/.test(message))
167
- return new StylePreviewError("ffmpeg-missing", "The style preview couldn't render because ffmpeg wasn't found. Install ffmpeg, or use the Docker image, then press Render again.");
167
+ return new StylePreviewError("ffmpeg-missing", "The style preview couldn't render because ffmpeg wasn't found. Install ffmpeg, or use the Docker image, then press Make the preview again.");
168
168
  log.write("error", "style-preview.render", { detail: message });
169
- return new StylePreviewError("render-failed", "The style preview couldn't render because ffmpeg stopped with an error. Press Render again; if it fails again, use Download diagnostics in Settings and report it.");
169
+ return new StylePreviewError("render-failed", "The style preview couldn't render because ffmpeg stopped with an error. Press Make the preview again; if it fails again, use Download diagnostics in Settings and report it.");
170
170
  }
@@ -60,7 +60,7 @@ export const stylePreviewRequestSchema = z.object({
60
60
  motionStyle: z.enum(motionStyles),
61
61
  })
62
62
  .optional(),
63
- // Render again even when this exact preview is already saved.
63
+ // Make the preview again even when this exact preview is already saved.
64
64
  force: z.boolean().optional(),
65
65
  });
66
66
  export const stylePreviewReplySchema = z.object({
@@ -24,7 +24,7 @@ export function captionCues(words) {
24
24
  !Number.isFinite(word.end) ||
25
25
  word.start < previousEnd - 0.001 ||
26
26
  word.end <= word.start) {
27
- throw new Error("The saved caption timing is damaged. Use More → Render the video again in the Video section to time them again, then Try again.");
27
+ throw new Error("The saved caption timing is damaged. Press Make the video again in the project's Video section to time them again, then Try again.");
28
28
  }
29
29
  const first = pending[0];
30
30
  if (first !== undefined &&
@@ -58,14 +58,24 @@ export async function getAnatomy(deps, id, section, item) {
58
58
  return {
59
59
  ok: false,
60
60
  reason: "not-found",
61
- message: "This template no longer exists; it may have been deleted. Go to Library → Templates to see your templates.",
61
+ message: "This template no longer exists; it may have been deleted. Go to Templates to see your templates.",
62
62
  };
63
63
  const record = sampleOf(deps.db, id);
64
64
  const fullScript = record?.fullScript ?? false;
65
- const resolved = await resolveSampleRun(deps.review, sampleDocument(template.document, {
65
+ const sampled = sampleDocument(template.document, {
66
66
  topic: record?.topic ?? defaultSampleTopic(deps.db),
67
67
  fullScript,
68
- }), fullScript);
68
+ });
69
+ let resolved = await resolveSampleRun(deps.review, sampled, fullScript);
70
+ // A refusal about another section (no image service on a fresh install) never hides this
71
+ // section's prompt: the setup is read again with only what this section's prompt needs.
72
+ for (const focused of resolved.ok ? [] : focusedDocuments(sampled, section)) {
73
+ const retry = await resolveSampleRun(deps.review, focused, fullScript);
74
+ if (retry.ok) {
75
+ resolved = retry;
76
+ break;
77
+ }
78
+ }
69
79
  const empty = (problem) => ({
70
80
  ok: true,
71
81
  value: { section, items: [], item: null, segments: [], fromSample: false, problem },
@@ -116,3 +126,36 @@ export async function getAnatomy(deps, id, section, item) {
116
126
  },
117
127
  };
118
128
  }
129
+ // The setup with every stage this section's prompt doesn't need turned off: first keeping the
130
+ // narration, which shapes the article's prompt (a script for speakers), then without it.
131
+ export function focusedDocuments(document, section) {
132
+ const { form } = document;
133
+ const keep = (own, source) => section === own ? source : "off";
134
+ const focused = {
135
+ ...document,
136
+ form: {
137
+ ...form,
138
+ sources: {
139
+ ...form.sources,
140
+ images: section === "shorts" ? form.sources.images : keep("pictures", form.sources.images),
141
+ thumbnail: keep("thumbnail", form.sources.thumbnail),
142
+ video: "off",
143
+ ...(form.sources.document === undefined ? {} : { document: "off" }),
144
+ },
145
+ youtubeDescription: section === "upload" ? form.youtubeDescription : false,
146
+ ...(form.shorts === undefined || section === "shorts"
147
+ ? {}
148
+ : { shorts: { ...form.shorts, enabled: false } }),
149
+ ...(section === "upload" ? {} : { social: undefined }),
150
+ ...(form.reviews === undefined || section === "reviews"
151
+ ? {}
152
+ : { reviews: { ...form.reviews, stages: {} } }),
153
+ },
154
+ };
155
+ if (section === "voice" || focused.form.sources.audio === "off")
156
+ return [focused];
157
+ return [
158
+ focused,
159
+ { ...focused, form: { ...focused.form, sources: { ...focused.form.sources, audio: "off" } } },
160
+ ];
161
+ }