@gentbajko/slopify 3.25.9 → 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 (718) hide show
  1. package/dist/adapters/account-limits.js +30 -0
  2. package/dist/adapters/alignment/audio.js +1 -1
  3. package/dist/adapters/alignment/cache.js +4 -6
  4. package/dist/adapters/alignment/ctc.js +1 -2
  5. package/dist/adapters/alignment/lock.js +4 -3
  6. package/dist/adapters/alignment/multilingual.js +2 -4
  7. package/dist/adapters/alignment/prefetch.js +2 -1
  8. package/dist/adapters/alignment/runner.js +1 -1
  9. package/dist/adapters/alignment/worker.js +3 -2
  10. package/dist/adapters/explain.js +10 -12
  11. package/dist/adapters/ffmpeg.js +19 -1
  12. package/dist/adapters/host-cli/index.js +21 -4
  13. package/dist/adapters/host-cli/transport.js +2 -1
  14. package/dist/adapters/http-failure.js +53 -0
  15. package/dist/adapters/image/bytes.js +2 -1
  16. package/dist/adapters/image/codex.js +42 -9
  17. package/dist/adapters/image/fal.js +21 -53
  18. package/dist/adapters/image/google.js +33 -56
  19. package/dist/adapters/image/models.js +2 -2
  20. package/dist/adapters/image/openai.js +22 -48
  21. package/dist/adapters/image/reference.js +36 -0
  22. package/dist/adapters/image/replicate.js +21 -49
  23. package/dist/adapters/image/video.js +2 -1
  24. package/dist/adapters/interest/search-interest.js +113 -0
  25. package/dist/adapters/key-probes.js +54 -3
  26. package/dist/adapters/llm/antigravity-models.js +1 -1
  27. package/dist/adapters/llm/antigravity.js +11 -6
  28. package/dist/adapters/llm/assistant-cli.js +388 -0
  29. package/dist/adapters/llm/child-session.js +83 -0
  30. package/dist/adapters/llm/claude-code-models.js +52 -99
  31. package/dist/adapters/llm/claude-code.js +16 -4
  32. package/dist/adapters/llm/codex-limits.js +27 -68
  33. package/dist/adapters/llm/codex-models.js +1 -1
  34. package/dist/adapters/llm/codex-server-models.js +61 -109
  35. package/dist/adapters/llm/codex.js +12 -1
  36. package/dist/adapters/llm/document-workspace.js +3 -2
  37. package/dist/adapters/llm/image-workspace.js +3 -2
  38. package/dist/adapters/llm/openrouter.js +12 -48
  39. package/dist/adapters/llm/run-cli.js +9 -24
  40. package/dist/adapters/llm/sse-lines.js +5 -1
  41. package/dist/adapters/model-access.js +37 -0
  42. package/dist/adapters/tts/cartesia.js +15 -46
  43. package/dist/adapters/tts/elevenlabs.js +20 -52
  44. package/dist/adapters/tts/gemini.js +21 -41
  45. package/dist/adapters/tts/inworld-async.js +6 -2
  46. package/dist/adapters/tts/inworld.js +6 -2
  47. package/dist/adapters/tts/openai.js +20 -50
  48. package/dist/adapters/tts/system.js +2 -1
  49. package/dist/assets/models-lifecycle.yaml +6 -0
  50. package/dist/assistant-wiring.js +60 -0
  51. package/dist/autopilot-timers.js +59 -0
  52. package/dist/catalog/lifecycle.js +52 -0
  53. package/dist/catalog/propose.js +204 -0
  54. package/dist/catalog/registry.js +23 -14
  55. package/dist/catalog/schema.js +4 -1
  56. package/dist/catalog/store.js +39 -0
  57. package/dist/catalog/validate.js +5 -2
  58. package/dist/edge/autostart/native.js +3 -5
  59. package/dist/edge/autostart/prompt.js +2 -1
  60. package/dist/edge/autostart/service.js +3 -4
  61. package/dist/edge/cli-args.js +3 -4
  62. package/dist/edge/cli.js +2 -1
  63. package/dist/edge/docker-install/apply.js +11 -11
  64. package/dist/edge/docker-install/run.js +2 -1
  65. package/dist/edge/docker-install/state.js +4 -3
  66. package/dist/edge/docker-install/tree.js +2 -4
  67. package/dist/edge/docker-install/volume.js +3 -2
  68. package/dist/edge/docker.js +5 -4
  69. package/dist/edge/events/hub.js +5 -5
  70. package/dist/edge/http/actions.js +11 -32
  71. package/dist/edge/http/actor.js +5 -3
  72. package/dist/edge/http/app.js +86 -11
  73. package/dist/edge/http/assistant.js +129 -0
  74. package/dist/edge/http/audio-preview.js +4 -11
  75. package/dist/edge/http/auditions.js +1 -1
  76. package/dist/edge/http/autostart.js +5 -5
  77. package/dist/edge/http/backups.js +2 -4
  78. package/dist/edge/http/channel-memory.js +9 -22
  79. package/dist/edge/http/channels.js +14 -27
  80. package/dist/edge/http/checkpoints.js +39 -21
  81. package/dist/edge/http/design-templates.js +167 -0
  82. package/dist/edge/http/designs.js +178 -0
  83. package/dist/edge/http/diagnostics.js +5 -1
  84. package/dist/edge/http/document-themes.js +5 -14
  85. package/dist/edge/http/draft-files.js +1 -3
  86. package/dist/edge/http/draft-problem.js +5 -10
  87. package/dist/edge/http/drafts.js +3 -3
  88. package/dist/edge/http/entries.js +6 -9
  89. package/dist/edge/http/extension-auth.js +27 -0
  90. package/dist/edge/http/external-reviews.js +12 -11
  91. package/dist/edge/http/files.js +5 -11
  92. package/dist/edge/http/folder-location.js +1 -4
  93. package/dist/edge/http/fonts.js +0 -7
  94. package/dist/edge/http/home.js +47 -14
  95. package/dist/edge/http/host-cli.js +18 -8
  96. package/dist/edge/http/ids.js +8 -0
  97. package/dist/edge/http/keywords.js +35 -0
  98. package/dist/edge/http/model-checks.js +132 -0
  99. package/dist/edge/http/narration-peaks.js +5 -13
  100. package/dist/edge/http/onboarding.js +30 -26
  101. package/dist/edge/http/open-folder.js +5 -10
  102. package/dist/edge/http/outputs.js +7 -14
  103. package/dist/edge/http/patch-notes.js +6 -12
  104. package/dist/edge/http/planning.js +3 -7
  105. package/dist/edge/http/problem.js +2 -4
  106. package/dist/edge/http/project-create.js +2 -1
  107. package/dist/edge/http/project-templates.js +31 -19
  108. package/dist/edge/http/projects.js +24 -16
  109. package/dist/edge/http/prompts.js +9 -16
  110. package/dist/edge/http/pronunciations.js +1 -2
  111. package/dist/edge/http/providers.js +48 -34
  112. package/dist/edge/http/refusal.js +29 -0
  113. package/dist/edge/http/remote-access.js +40 -0
  114. package/dist/edge/http/remote-guard.js +71 -0
  115. package/dist/edge/http/request-guard.js +91 -0
  116. package/dist/edge/http/reviews.js +12 -25
  117. package/dist/edge/http/revision-files.js +6 -11
  118. package/dist/edge/http/revisions.js +14 -19
  119. package/dist/edge/http/run-cost.js +1 -2
  120. package/dist/edge/http/same-page.js +80 -0
  121. package/dist/edge/http/schedules.js +69 -40
  122. package/dist/edge/http/settings.js +27 -14
  123. package/dist/edge/http/social.js +283 -0
  124. package/dist/edge/http/staging.js +3 -5
  125. package/dist/edge/http/storage-files.js +5 -7
  126. package/dist/edge/http/storage.js +33 -25
  127. package/dist/edge/http/studio.js +94 -69
  128. package/dist/edge/http/style-preview.js +3 -6
  129. package/dist/edge/http/subtitles.js +3 -17
  130. package/dist/edge/http/template-studio.js +436 -0
  131. package/dist/edge/http/trash.js +11 -12
  132. package/dist/edge/http/tutorial.js +1 -2
  133. package/dist/edge/http/tutorials.js +5 -11
  134. package/dist/edge/http/update.js +7 -13
  135. package/dist/edge/http/waveform.js +3 -6
  136. package/dist/edge/http/youtube-edits.js +3 -4
  137. package/dist/edge/mcp/assistant.js +279 -0
  138. package/dist/edge/mcp/attribution.js +8 -2
  139. package/dist/edge/mcp/call-route.js +11 -2
  140. package/dist/edge/mcp/excluded-routes.js +29 -3
  141. package/dist/edge/mcp/http.js +11 -2
  142. package/dist/edge/mcp/instructions.js +6 -4
  143. package/dist/edge/mcp/library-tools.js +9 -5
  144. package/dist/edge/mcp/remake-tools.js +7 -4
  145. package/dist/edge/mcp/route-tool.js +35 -15
  146. package/dist/edge/mcp/server.js +13 -8
  147. package/dist/edge/mcp/tools-channels.js +11 -9
  148. package/dist/edge/mcp/tools-designs.js +165 -0
  149. package/dist/edge/mcp/tools-library.js +19 -7
  150. package/dist/edge/mcp/tools-planning.js +40 -21
  151. package/dist/edge/mcp/tools-projects.js +38 -11
  152. package/dist/edge/mcp/tools-publishing.js +9 -9
  153. package/dist/edge/mcp/tools-runs.js +23 -10
  154. package/dist/edge/mcp/tools-schedules.js +32 -8
  155. package/dist/edge/mcp/tools-setup.js +75 -11
  156. package/dist/edge/mcp/tools-social.js +47 -0
  157. package/dist/edge/mcp/tools-storage.js +11 -3
  158. package/dist/edge/mcp/tools-template-studio.js +166 -0
  159. package/dist/edge/mcp/tools.js +6 -0
  160. package/dist/edge/remote-access/cli.js +71 -0
  161. package/dist/edge/remote-access/model.js +32 -0
  162. package/dist/edge/remote-access/rules.js +207 -0
  163. package/dist/edge/remote-access/service.js +203 -0
  164. package/dist/edge/remote-access/store.js +44 -0
  165. package/dist/edge/remote-access/views.js +43 -0
  166. package/dist/extension/slopify-studio-chrome.zip +0 -0
  167. package/dist/extension/slopify-studio-firefox.zip +0 -0
  168. package/dist/host-cli/install.js +2 -2
  169. package/dist/host-cli/open-folder.js +2 -2
  170. package/dist/host-cli/paths.js +2 -4
  171. package/dist/host-cli/server.js +4 -4
  172. package/dist/host-cli/service.js +3 -3
  173. package/dist/host-cli/status.js +7 -7
  174. package/dist/kernel/db/migrations/0063-model-checks.sql +20 -0
  175. package/dist/kernel/db/migrations/0064-template-studio.sql +46 -0
  176. package/dist/kernel/db/migrations/0065-promise-and-assistant-reviews.sql +58 -0
  177. package/dist/kernel/db/migrations/0066-assistant.sql +44 -0
  178. package/dist/kernel/db/migrations/0067-autopilot.sql +6 -0
  179. package/dist/kernel/db/migrations/0068-topic-scores.sql +17 -0
  180. package/dist/kernel/db/migrations/0070-designs.sql +53 -0
  181. package/dist/kernel/db/migrations/0071-design-usage.sql +33 -0
  182. package/dist/kernel/db/migrations/0072-design-templates.sql +33 -0
  183. package/dist/kernel/db/migrations/0073-schedule-design-kind.sql +5 -0
  184. package/dist/kernel/db/migrations/0074-template-bakeoffs.sql +44 -0
  185. package/dist/kernel/db/migrations/0075-social-posts.sql +25 -0
  186. package/dist/kernel/db/migrations/0076-channel-social.sql +5 -0
  187. package/dist/kernel/db/migrations/0077-post-prompts.sql +17 -0
  188. package/dist/kernel/db/migrations/0078-uploaded-from-links.sql +7 -0
  189. package/dist/kernel/db/tx.js +2 -4
  190. package/dist/kernel/errors.js +27 -1
  191. package/dist/kernel/json-answer.js +15 -0
  192. package/dist/kernel/lock.js +4 -9
  193. package/dist/kernel/paths.js +8 -0
  194. package/dist/kernel/pipeline.js +12 -0
  195. package/dist/kernel/ports/assistant-cli.js +4 -0
  196. package/dist/kernel/ports/host-cli.js +3 -0
  197. package/dist/kernel/ports/interest.js +4 -0
  198. package/dist/kernel/ports/languages.js +0 -3
  199. package/dist/kernel/ports/model.js +2 -0
  200. package/dist/kernel/ports/settings-places.js +31 -0
  201. package/dist/kernel/ports/system-speech.js +31 -3
  202. package/dist/kernel/runner/attempt.js +14 -4
  203. package/dist/kernel/runner/audition.js +3 -1
  204. package/dist/kernel/runner/checkpoint-authority.js +2 -1
  205. package/dist/kernel/runner/index.js +8 -10
  206. package/dist/kernel/runner/model-access.js +40 -0
  207. package/dist/kernel/runner/model-check.js +153 -0
  208. package/dist/kernel/runner/providers.js +18 -12
  209. package/dist/kernel/runner/standalone.js +11 -7
  210. package/dist/main.js +338 -21
  211. package/dist/model-catalog.js +20 -0
  212. package/dist/model-checks.js +166 -0
  213. package/dist/patch-notes/4.0.0.md +51 -0
  214. package/dist/patch-notes/4.1.0.md +42 -0
  215. package/dist/patch-notes/index.json +12 -0
  216. package/dist/slices/admission/heads.js +2 -1
  217. package/dist/slices/admission/listing-facts.js +95 -0
  218. package/dist/slices/admission/repo.js +11 -7
  219. package/dist/slices/admission/rules.js +50 -15
  220. package/dist/slices/admission/schema.js +8 -0
  221. package/dist/slices/admission/stage-writes.js +0 -3
  222. package/dist/slices/admission/start.js +48 -31
  223. package/dist/slices/admission/state-words.js +34 -5
  224. package/dist/slices/admission/substitute.js +7 -4
  225. package/dist/slices/agents/attention-posts.js +31 -0
  226. package/dist/slices/agents/attention-templates.js +33 -0
  227. package/dist/slices/agents/attention.js +10 -1
  228. package/dist/slices/agents/connection.js +2 -6
  229. package/dist/slices/agents/model.js +6 -3
  230. package/dist/slices/agents/repo.js +15 -7
  231. package/dist/slices/article/continuation.js +0 -63
  232. package/dist/slices/article/plain.js +3 -2
  233. package/dist/slices/article/segments.js +2 -62
  234. package/dist/slices/assistant/access.js +1 -0
  235. package/dist/slices/assistant/asks.js +80 -0
  236. package/dist/slices/assistant/cost.js +54 -0
  237. package/dist/slices/assistant/gate.js +222 -0
  238. package/dist/slices/assistant/model.js +86 -0
  239. package/dist/slices/assistant/names.js +23 -0
  240. package/dist/slices/assistant/prompt.js +74 -0
  241. package/dist/slices/assistant/repo.js +74 -0
  242. package/dist/slices/assistant/tokens.js +18 -0
  243. package/dist/slices/assistant/turn.js +344 -0
  244. package/dist/slices/autopilot/buffer.js +76 -0
  245. package/dist/slices/autopilot/model.js +14 -0
  246. package/dist/slices/autopilot/notify.js +27 -0
  247. package/dist/slices/autopilot/repo.js +34 -0
  248. package/dist/slices/autopilot/signals.js +194 -0
  249. package/dist/slices/autopilot/warnings.js +85 -0
  250. package/dist/slices/autopilot/weekly.js +100 -0
  251. package/dist/slices/backups/files.js +2 -6
  252. package/dist/slices/backups/repo.js +3 -15
  253. package/dist/slices/backups/service.js +8 -7
  254. package/dist/slices/channels/cast-images.js +3 -2
  255. package/dist/slices/channels/model.js +0 -1
  256. package/dist/slices/channels/rebrand.js +0 -3
  257. package/dist/slices/channels/repo.js +4 -0
  258. package/dist/slices/channels/schema.js +7 -1
  259. package/dist/slices/channels/service.js +10 -0
  260. package/dist/slices/channels/videos.js +3 -15
  261. package/dist/slices/checkpoints/index.js +0 -1
  262. package/dist/slices/control/providers.js +2 -28
  263. package/dist/slices/designs/artwork.js +66 -0
  264. package/dist/slices/designs/budget.js +23 -0
  265. package/dist/slices/designs/calls.js +124 -0
  266. package/dist/slices/designs/check-context.js +38 -0
  267. package/dist/slices/designs/check-text.js +43 -0
  268. package/dist/slices/designs/fields.js +88 -0
  269. package/dist/slices/designs/frames.js +34 -0
  270. package/dist/slices/designs/gate.js +130 -0
  271. package/dist/slices/designs/layout.js +293 -0
  272. package/dist/slices/designs/model.js +35 -0
  273. package/dist/slices/designs/pdf-job.js +23 -0
  274. package/dist/slices/designs/pdf-pages.js +30 -0
  275. package/dist/slices/designs/pdf-runner.js +68 -0
  276. package/dist/slices/designs/pdf-worker.js +24 -0
  277. package/dist/slices/designs/picture-size.js +79 -0
  278. package/dist/slices/designs/pictures.js +55 -0
  279. package/dist/slices/designs/quote.js +24 -0
  280. package/dist/slices/designs/render-args.js +36 -0
  281. package/dist/slices/designs/render-input.js +59 -0
  282. package/dist/slices/designs/render.js +215 -0
  283. package/dist/slices/designs/repo.js +101 -0
  284. package/dist/slices/designs/requests.js +80 -0
  285. package/dist/slices/designs/runner.js +239 -0
  286. package/dist/slices/designs/sample-repo.js +91 -0
  287. package/dist/slices/designs/sample.js +223 -0
  288. package/dist/slices/designs/schema.js +87 -0
  289. package/dist/slices/designs/sections.js +44 -0
  290. package/dist/slices/designs/service.js +233 -0
  291. package/dist/slices/designs/steps-repo.js +102 -0
  292. package/dist/slices/designs/studio-kind.js +39 -0
  293. package/dist/slices/designs/template-checks.js +44 -0
  294. package/dist/slices/designs/template-make.js +118 -0
  295. package/dist/slices/designs/templates.js +218 -0
  296. package/dist/slices/document/fonts.js +2 -1
  297. package/dist/slices/document/model.js +7 -4
  298. package/dist/slices/document/render.js +2 -1
  299. package/dist/slices/document/theme-schema.js +1 -1
  300. package/dist/slices/document/theme.js +49 -3
  301. package/dist/slices/episodes/summarize.js +6 -2
  302. package/dist/slices/estimate/index.js +72 -28
  303. package/dist/slices/estimate/requests.js +4 -4
  304. package/dist/slices/fixes/rules.js +3 -1
  305. package/dist/slices/fonts/catalog.js +5 -3
  306. package/dist/slices/fonts/coverage.js +2 -1
  307. package/dist/slices/fonts/discovery.js +3 -2
  308. package/dist/slices/fonts/files.js +2 -6
  309. package/dist/slices/fonts/index.js +0 -1
  310. package/dist/slices/fonts/sfnt.js +45 -10
  311. package/dist/slices/images/appearance.js +8 -16
  312. package/dist/slices/images/scenes.js +102 -28
  313. package/dist/slices/keywords/digest.js +85 -0
  314. package/dist/slices/keywords/facts.js +92 -0
  315. package/dist/slices/keywords/fill.js +80 -0
  316. package/dist/slices/keywords/model.js +75 -0
  317. package/dist/slices/keywords/rules.js +187 -0
  318. package/dist/slices/keywords/schema.js +28 -0
  319. package/dist/slices/keywords/specs.js +260 -0
  320. package/dist/slices/keywords/values.js +74 -0
  321. package/dist/slices/library/lint.js +29 -12
  322. package/dist/slices/library/model.js +2 -0
  323. package/dist/slices/library/photorealistic.js +4 -12
  324. package/dist/slices/library/rename.js +9 -7
  325. package/dist/slices/library/save.js +14 -10
  326. package/dist/slices/library/slots.js +37 -11
  327. package/dist/slices/library/transfer.js +5 -4
  328. package/dist/slices/library/used-by.js +25 -10
  329. package/dist/slices/loudness/loudnorm.js +53 -12
  330. package/dist/slices/loudness/model.js +3 -2
  331. package/dist/slices/loudness/sentence-level.js +2 -1
  332. package/dist/slices/model-checks/cost.js +61 -0
  333. package/dist/slices/model-checks/listing.js +124 -0
  334. package/dist/slices/model-checks/model.js +20 -0
  335. package/dist/slices/model-checks/record.js +32 -0
  336. package/dist/slices/model-checks/repo.js +120 -0
  337. package/dist/slices/model-checks/rules.js +125 -0
  338. package/dist/slices/model-checks/run.js +143 -0
  339. package/dist/slices/model-checks/service.js +198 -0
  340. package/dist/slices/model-upkeep/model.js +1 -1
  341. package/dist/slices/model-upkeep/switch.js +72 -27
  342. package/dist/slices/model-upkeep/usage.js +39 -13
  343. package/dist/slices/narration/breaths.js +3 -1
  344. package/dist/slices/narration/clean-pieces.js +3 -1
  345. package/dist/slices/narration/concat.js +2 -1
  346. package/dist/slices/narration/pauses-model.js +3 -2
  347. package/dist/slices/narration/peaks.js +3 -4
  348. package/dist/slices/narration/pronunciation-chunks.js +4 -3
  349. package/dist/slices/narration/pronunciation.js +6 -4
  350. package/dist/slices/narration/steering.js +4 -3
  351. package/dist/slices/notifications/notifier.js +4 -3
  352. package/dist/slices/notifications/rules.js +5 -2
  353. package/dist/slices/notifications/settings.js +4 -12
  354. package/dist/slices/notifications/subject.js +4 -3
  355. package/dist/slices/onboarding/copy.js +21 -3
  356. package/dist/slices/onboarding/first-run.js +16 -40
  357. package/dist/slices/onboarding/full-video.js +3 -2
  358. package/dist/slices/onboarding/install.js +6 -4
  359. package/dist/slices/onboarding/model.js +26 -37
  360. package/dist/slices/onboarding/packs.js +22 -22
  361. package/dist/slices/onboarding/preferred-model.js +14 -0
  362. package/dist/slices/onboarding/quick-short.js +105 -65
  363. package/dist/slices/onboarding/sample.js +4 -2
  364. package/dist/slices/onboarding/state.js +9 -11
  365. package/dist/slices/onboarding/template.js +8 -6
  366. package/dist/slices/outputs/add.js +3 -2
  367. package/dist/slices/outputs/service.js +2 -2
  368. package/dist/slices/patch-notes/library.js +4 -3
  369. package/dist/slices/play-drafts/convert.js +4 -0
  370. package/dist/slices/play-drafts/readiness.js +3 -3
  371. package/dist/slices/play-drafts/review-inputs.js +9 -3
  372. package/dist/slices/play-drafts/review.js +5 -4
  373. package/dist/slices/play-drafts/schema.js +4 -0
  374. package/dist/slices/play-drafts/service.js +17 -7
  375. package/dist/slices/project-templates/from-project.js +9 -2
  376. package/dist/slices/project-templates/next-chapter.js +2 -1
  377. package/dist/slices/project-templates/provider-defaults.js +22 -0
  378. package/dist/slices/project-templates/repo.js +6 -1
  379. package/dist/slices/project-templates/setup-sections.js +26 -0
  380. package/dist/slices/project-templates/setup.js +14 -1
  381. package/dist/slices/project-templates/stand-ins.js +22 -0
  382. package/dist/slices/rebuild/activity.js +2 -0
  383. package/dist/slices/rebuild/admission-repo.js +3 -2
  384. package/dist/slices/rebuild/narration-history.js +4 -5
  385. package/dist/slices/rebuild/narration-retry.js +2 -1
  386. package/dist/slices/rebuild/preview-details.js +32 -14
  387. package/dist/slices/rebuild/preview-plan.js +3 -8
  388. package/dist/slices/rebuild/recipe-build.js +8 -3
  389. package/dist/slices/rebuild/recipe-describe.js +3 -3
  390. package/dist/slices/rebuild/recipe-digest.js +58 -0
  391. package/dist/slices/rebuild/recipe-input-schema.js +10 -0
  392. package/dist/slices/rebuild/recipe-keywords.js +71 -0
  393. package/dist/slices/rebuild/recipe-legacy.js +5 -1
  394. package/dist/slices/rebuild/recipe-model.js +4 -0
  395. package/dist/slices/rebuild/recipe-narration-text.js +2 -1
  396. package/dist/slices/rebuild/recipe-preparation.js +4 -4
  397. package/dist/slices/rebuild/recipe-reference.js +8 -4
  398. package/dist/slices/rebuild/recipe-reviews.js +27 -7
  399. package/dist/slices/rebuild/recipe-save.js +8 -5
  400. package/dist/slices/rebuild/recipe-scenes.js +40 -11
  401. package/dist/slices/rebuild/recipe-shorts.js +8 -8
  402. package/dist/slices/rebuild/recipe-social.js +46 -0
  403. package/dist/slices/rebuild/recipe-text.js +64 -20
  404. package/dist/slices/rebuild/recipe-validation.js +102 -31
  405. package/dist/slices/rebuild/recipe-visual.js +55 -16
  406. package/dist/slices/rebuild/recipe-voices.js +3 -2
  407. package/dist/slices/rebuild/recipe-work.js +19 -7
  408. package/dist/slices/rebuild/recipe-youtube.js +4 -3
  409. package/dist/slices/rebuild/recovery-selection.js +3 -1
  410. package/dist/slices/rebuild/recovery.js +9 -8
  411. package/dist/slices/rebuild/remake-outdated.js +5 -5
  412. package/dist/slices/rebuild/repo.js +4 -3
  413. package/dist/slices/rebuild/review-promise.js +25 -0
  414. package/dist/slices/rebuild/review-redo.js +3 -2
  415. package/dist/slices/rebuild/runtime-admission.js +4 -3
  416. package/dist/slices/rebuild/runtime-agent-review.js +11 -5
  417. package/dist/slices/rebuild/runtime-animate.js +10 -9
  418. package/dist/slices/rebuild/runtime-article.js +7 -5
  419. package/dist/slices/rebuild/runtime-checkpoints.js +5 -4
  420. package/dist/slices/rebuild/runtime-document.js +11 -13
  421. package/dist/slices/rebuild/runtime-export-bed.js +8 -12
  422. package/dist/slices/rebuild/runtime-export-edit.js +8 -16
  423. package/dist/slices/rebuild/runtime-export-inputs.js +12 -11
  424. package/dist/slices/rebuild/runtime-export-short.js +9 -5
  425. package/dist/slices/rebuild/runtime-export.js +38 -15
  426. package/dist/slices/rebuild/runtime-figure-card.js +2 -1
  427. package/dist/slices/rebuild/runtime-image.js +8 -12
  428. package/dist/slices/rebuild/runtime-instructions.js +2 -0
  429. package/dist/slices/rebuild/runtime-keywords.js +59 -0
  430. package/dist/slices/rebuild/runtime-lines.js +2 -11
  431. package/dist/slices/rebuild/runtime-local.js +17 -22
  432. package/dist/slices/rebuild/runtime-narration-publication.js +2 -5
  433. package/dist/slices/rebuild/runtime-narration-reuse.js +9 -13
  434. package/dist/slices/rebuild/runtime-narration-text.js +13 -17
  435. package/dist/slices/rebuild/runtime-pictures.js +2 -4
  436. package/dist/slices/rebuild/runtime-piece-label.js +10 -6
  437. package/dist/slices/rebuild/runtime-plan.js +31 -5
  438. package/dist/slices/rebuild/runtime-provider.js +67 -25
  439. package/dist/slices/rebuild/runtime-publication.js +10 -9
  440. package/dist/slices/rebuild/runtime-review.js +39 -33
  441. package/dist/slices/rebuild/runtime-run.js +4 -1
  442. package/dist/slices/rebuild/runtime-shorts.js +31 -50
  443. package/dist/slices/rebuild/runtime-store.js +20 -10
  444. package/dist/slices/rebuild/runtime-subtitles.js +28 -25
  445. package/dist/slices/rebuild/runtime-voices.js +10 -16
  446. package/dist/slices/rebuild/runtime-youtube.js +14 -31
  447. package/dist/slices/rebuild/service-readiness.js +11 -3
  448. package/dist/slices/rebuild/transition-repo.js +6 -5
  449. package/dist/slices/rebuild/work-records.js +6 -0
  450. package/dist/slices/reruns/cascade.js +1 -2
  451. package/dist/slices/reruns/index.js +6 -9
  452. package/dist/slices/research/synthesis.js +3 -2
  453. package/dist/slices/reviews/external-model.js +4 -1
  454. package/dist/slices/reviews/external-repo.js +2 -1
  455. package/dist/slices/reviews/external-rows.js +2 -3
  456. package/dist/slices/reviews/holds.js +5 -4
  457. package/dist/slices/reviews/model.js +32 -11
  458. package/dist/slices/reviews/repo.js +2 -1
  459. package/dist/slices/reviews/rules.js +4 -3
  460. package/dist/slices/reviews/verdict.js +2 -1
  461. package/dist/slices/reviews/view.js +5 -2
  462. package/dist/slices/revisions/downloads.js +4 -5
  463. package/dist/slices/revisions/image-plan.js +1 -1
  464. package/dist/slices/revisions/mutation-assets.js +9 -5
  465. package/dist/slices/revisions/mutation-cues.js +2 -5
  466. package/dist/slices/revisions/mutations.js +3 -3
  467. package/dist/slices/revisions/repo.js +3 -1
  468. package/dist/slices/revisions/schema.js +1 -0
  469. package/dist/slices/revisions/view.js +20 -0
  470. package/dist/slices/run-cost/limits.js +5 -2
  471. package/dist/slices/run-cost/meter.js +12 -5
  472. package/dist/slices/run-cost/panel.js +8 -74
  473. package/dist/slices/run-cost/plan-readings.js +127 -0
  474. package/dist/slices/run-cost/plan-use.js +169 -0
  475. package/dist/slices/run-cost/pricing.js +3 -3
  476. package/dist/slices/run-cost/week.js +32 -4
  477. package/dist/slices/schedules/agenda.js +18 -7
  478. package/dist/slices/schedules/by-date.js +42 -0
  479. package/dist/slices/schedules/calendar.js +5 -0
  480. package/dist/slices/schedules/design-agenda.js +36 -0
  481. package/dist/slices/schedules/design-check.js +41 -0
  482. package/dist/slices/schedules/design-run.js +126 -0
  483. package/dist/slices/schedules/next-run.js +62 -0
  484. package/dist/slices/schedules/prepare.js +18 -2
  485. package/dist/slices/schedules/repo.js +30 -21
  486. package/dist/slices/schedules/rescore.js +35 -0
  487. package/dist/slices/schedules/scheduler.js +13 -3
  488. package/dist/slices/schedules/schema.js +57 -3
  489. package/dist/slices/schedules/score.js +107 -0
  490. package/dist/slices/schedules/service.js +57 -24
  491. package/dist/slices/schedules/topic-list.js +15 -0
  492. package/dist/slices/schedules/topic-scores.js +104 -0
  493. package/dist/slices/schedules/topics.js +53 -41
  494. package/dist/slices/settings/cli-paths.js +4 -6
  495. package/dist/slices/settings/first-run.js +8 -43
  496. package/dist/slices/settings/health.js +30 -15
  497. package/dist/slices/settings/key-test.js +1 -1
  498. package/dist/slices/settings/model-reach.js +3 -17
  499. package/dist/slices/settings/model.js +14 -2
  500. package/dist/slices/settings/playback.js +34 -23
  501. package/dist/slices/settings/probe-read.js +16 -0
  502. package/dist/slices/settings/repo.js +33 -0
  503. package/dist/slices/settings/tutorial.js +2 -2
  504. package/dist/slices/settings/voices.js +4 -12
  505. package/dist/slices/shorts/keep.js +2 -1
  506. package/dist/slices/shorts/pick.js +6 -18
  507. package/dist/slices/shorts/prompts.js +4 -14
  508. package/dist/slices/shorts/render.js +6 -2
  509. package/dist/slices/social/channel.js +44 -0
  510. package/dist/slices/social/defaults.js +17 -0
  511. package/dist/slices/social/due.js +36 -0
  512. package/dist/slices/social/extension.js +18 -0
  513. package/dist/slices/social/fit.js +115 -0
  514. package/dist/slices/social/limits.js +186 -0
  515. package/dist/slices/social/links.js +21 -0
  516. package/dist/slices/social/media.js +132 -0
  517. package/dist/slices/social/model.js +75 -0
  518. package/dist/slices/social/notice.js +19 -0
  519. package/dist/slices/social/pack.js +176 -0
  520. package/dist/slices/social/post-prompt.js +56 -0
  521. package/dist/slices/social/queue.js +31 -0
  522. package/dist/slices/social/repo.js +133 -0
  523. package/dist/slices/social/reshape.js +97 -0
  524. package/dist/slices/social/rules.js +5 -0
  525. package/dist/slices/social/schema.js +70 -0
  526. package/dist/slices/social/serve.js +51 -0
  527. package/dist/slices/social/service.js +129 -0
  528. package/dist/slices/social/settings.js +21 -0
  529. package/dist/slices/social/sources.js +114 -0
  530. package/dist/slices/social/state.js +110 -0
  531. package/dist/slices/social/text.js +149 -0
  532. package/dist/slices/storage/arrange.js +3 -1
  533. package/dist/slices/storage/assets.js +8 -10
  534. package/dist/slices/storage/backup-designs.js +74 -0
  535. package/dist/slices/storage/backup-export.js +8 -3
  536. package/dist/slices/storage/backup-format.js +10 -0
  537. package/dist/slices/storage/backup-import.js +22 -4
  538. package/dist/slices/storage/delete-project.js +6 -5
  539. package/dist/slices/storage/files-location.js +6 -8
  540. package/dist/slices/storage/layout.js +13 -2
  541. package/dist/slices/storage/model.js +3 -0
  542. package/dist/slices/storage/portable.js +17 -12
  543. package/dist/slices/storage/schema.js +2 -0
  544. package/dist/slices/storage/staging-refs.js +2 -1
  545. package/dist/slices/storage/staging.js +6 -9
  546. package/dist/slices/storage/trim.js +26 -9
  547. package/dist/slices/studio/calendar.js +7 -2
  548. package/dist/slices/studio/disclosure.js +4 -4
  549. package/dist/slices/studio/model.js +1 -1
  550. package/dist/slices/studio/pack.js +9 -8
  551. package/dist/slices/studio/pick.js +2 -11
  552. package/dist/slices/studio/plan.js +7 -16
  553. package/dist/slices/studio/queue.js +3 -14
  554. package/dist/slices/studio/releases.js +26 -0
  555. package/dist/slices/studio/settings.js +20 -57
  556. package/dist/slices/studio/videos.js +35 -4
  557. package/dist/slices/style-preview/render.js +37 -16
  558. package/dist/slices/style-preview/schema.js +14 -1
  559. package/dist/slices/style-preview/service.js +2 -1
  560. package/dist/slices/style-preview/settings.js +33 -5
  561. package/dist/slices/subtitles/captions.js +1 -1
  562. package/dist/slices/telemetry/collector-client.js +2 -4
  563. package/dist/slices/telemetry/flush.js +2 -4
  564. package/dist/slices/telemetry/model.js +0 -7
  565. package/dist/slices/telemetry/record.js +10 -3
  566. package/dist/slices/template-studio/anatomy-view.js +161 -0
  567. package/dist/slices/template-studio/anatomy.js +73 -0
  568. package/dist/slices/template-studio/bakeoff-make.js +225 -0
  569. package/dist/slices/template-studio/bakeoff-repo.js +83 -0
  570. package/dist/slices/template-studio/bakeoff.js +333 -0
  571. package/dist/slices/template-studio/budget.js +106 -0
  572. package/dist/slices/template-studio/cards.js +78 -0
  573. package/dist/slices/template-studio/checks.js +322 -0
  574. package/dist/slices/template-studio/document-sample.js +56 -0
  575. package/dist/slices/template-studio/edit.js +15 -0
  576. package/dist/slices/template-studio/for-all.js +161 -0
  577. package/dist/slices/template-studio/free-video.js +31 -0
  578. package/dist/slices/template-studio/free.js +176 -0
  579. package/dist/slices/template-studio/gate.js +40 -0
  580. package/dist/slices/template-studio/hidden.js +20 -0
  581. package/dist/slices/template-studio/jobs.js +79 -0
  582. package/dist/slices/template-studio/loudness.js +111 -0
  583. package/dist/slices/template-studio/make.js +40 -0
  584. package/dist/slices/template-studio/model.js +58 -0
  585. package/dist/slices/template-studio/pack-import.js +253 -0
  586. package/dist/slices/template-studio/pack-samples.js +60 -0
  587. package/dist/slices/template-studio/pack.js +207 -0
  588. package/dist/slices/template-studio/paid-view.js +66 -0
  589. package/dist/slices/template-studio/paid.js +355 -0
  590. package/dist/slices/template-studio/quote.js +155 -0
  591. package/dist/slices/template-studio/ready.js +104 -0
  592. package/dist/slices/template-studio/repo.js +134 -0
  593. package/dist/slices/template-studio/reviews-path.js +58 -0
  594. package/dist/slices/template-studio/runner.js +84 -0
  595. package/dist/slices/template-studio/save.js +236 -0
  596. package/dist/slices/template-studio/say-it-as.js +248 -0
  597. package/dist/slices/template-studio/schedule-gate.js +34 -0
  598. package/dist/slices/template-studio/schedule-price.js +32 -0
  599. package/dist/slices/template-studio/schedule-sample.js +91 -0
  600. package/dist/slices/template-studio/schema.js +243 -0
  601. package/dist/slices/template-studio/scope.js +124 -0
  602. package/dist/slices/template-studio/sections.js +176 -0
  603. package/dist/slices/template-studio/service.js +175 -0
  604. package/dist/slices/template-studio/status.js +191 -0
  605. package/dist/slices/template-studio/topic.js +223 -0
  606. package/dist/slices/thumbnail/by-llm.js +1 -1
  607. package/dist/slices/trash/designs.js +76 -0
  608. package/dist/slices/trash/model.js +13 -4
  609. package/dist/slices/trash/service.js +52 -2
  610. package/dist/slices/tutorials/library.js +3 -2
  611. package/dist/slices/uploads/repo.js +37 -1
  612. package/dist/slices/uploads/rules.js +3 -2
  613. package/dist/slices/video/ass-text.js +17 -0
  614. package/dist/slices/video/audio-export.js +2 -64
  615. package/dist/slices/video/edit-list.js +2 -2
  616. package/dist/slices/video/edit-settings.js +1 -1
  617. package/dist/slices/video/encoding.js +169 -0
  618. package/dist/slices/video/ffmpeg.js +23 -21
  619. package/dist/slices/video/figure-card.js +1 -15
  620. package/dist/slices/video/plan.js +2 -1
  621. package/dist/slices/video/slideshow.js +43 -18
  622. package/dist/slices/voices/attribution.js +3 -2
  623. package/dist/slices/voices/model.js +9 -8
  624. package/dist/slices/voices/palette.js +0 -3
  625. package/dist/slices/voices/script.js +2 -1
  626. package/dist/slices/youtube/answer.js +4 -16
  627. package/dist/slices/youtube/edits-repo.js +3 -12
  628. package/dist/tutorials/AI-CLIs.md +25 -25
  629. package/dist/tutorials/Agents-MCP.md +34 -33
  630. package/dist/tutorials/Assistant.md +42 -0
  631. package/dist/tutorials/Backups.md +7 -7
  632. package/dist/tutorials/Calendar.md +17 -25
  633. package/dist/tutorials/Cast-Library.md +19 -19
  634. package/dist/tutorials/Channels.md +42 -35
  635. package/dist/tutorials/Costs-and-Run-Cost.md +38 -40
  636. package/dist/tutorials/Create-Article.md +114 -0
  637. package/dist/tutorials/Create-Extras.md +112 -0
  638. package/dist/tutorials/Create-Images.md +165 -0
  639. package/dist/tutorials/Create-Narration.md +178 -0
  640. package/dist/tutorials/Create-Overview.md +244 -0
  641. package/dist/tutorials/Create-Video-and-Style.md +144 -0
  642. package/dist/tutorials/Docker.md +3 -3
  643. package/dist/tutorials/Document-Themes.md +32 -32
  644. package/dist/tutorials/Editing-a-Project.md +94 -94
  645. package/dist/tutorials/FAQ.md +18 -18
  646. package/dist/tutorials/First-Launch-and-Welcome.md +50 -64
  647. package/dist/tutorials/Home-and-Projects.md +115 -71
  648. package/dist/tutorials/Home.md +33 -33
  649. package/dist/tutorials/Install.md +14 -13
  650. package/dist/tutorials/Intros-and-Outros.md +33 -34
  651. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +62 -58
  652. package/dist/tutorials/Keywords-Slopify-Fills.md +87 -0
  653. package/dist/tutorials/Models.md +53 -21
  654. package/dist/tutorials/Multiple-Voices.md +41 -40
  655. package/dist/tutorials/Narration-Aliases-and-Glossary.md +14 -14
  656. package/dist/tutorials/Notifications.md +27 -17
  657. package/dist/tutorials/Other-Languages.md +15 -15
  658. package/dist/tutorials/PDF-Documents.md +38 -29
  659. package/dist/tutorials/Posting-to-Social-Sites.md +121 -0
  660. package/dist/tutorials/Project-Page.md +118 -120
  661. package/dist/tutorials/Prompts-and-Templates.md +84 -0
  662. package/dist/tutorials/Prompts.md +48 -44
  663. package/dist/tutorials/Providers-and-Keys.md +105 -98
  664. package/dist/tutorials/Publishing-to-YouTube.md +31 -31
  665. package/dist/tutorials/Recovery-and-Retries.md +31 -31
  666. package/dist/tutorials/Remote-Access.md +63 -0
  667. package/dist/tutorials/Reviews-and-Checkpoints.md +98 -90
  668. package/dist/tutorials/Schedules.md +46 -27
  669. package/dist/tutorials/Settings-Reference.md +194 -174
  670. package/dist/tutorials/Shorts.md +34 -33
  671. package/dist/tutorials/Start-at-Login.md +3 -3
  672. package/dist/tutorials/Studio-Extension.md +22 -24
  673. package/dist/tutorials/Template-Studio.md +128 -0
  674. package/dist/tutorials/Templates.md +51 -77
  675. package/dist/tutorials/Trash.md +4 -4
  676. package/dist/tutorials/Troubleshooting.md +44 -44
  677. package/dist/tutorials/Uninstalling-and-Moving.md +5 -5
  678. package/dist/tutorials/Updating-and-Patch-Notes.md +18 -18
  679. package/dist/tutorials/Video-Editing.md +34 -34
  680. package/dist/tutorials/Where-Your-Files-Live.md +10 -10
  681. package/dist/tutorials/YouTube-Description.md +26 -26
  682. package/dist/tutorials/Your-First-Short.md +35 -35
  683. package/dist/tutorials/_Sidebar.md +19 -14
  684. package/dist/updater/work-in-progress.js +42 -12
  685. package/dist/web/assets/index-BNftKQrA.css +1 -0
  686. package/dist/web/assets/index-Cpa58woP.js +459 -0
  687. package/dist/web/assets/pdf-BKh8f7R6.js +56 -0
  688. package/dist/web/assets/pdf.worker.min-BmVo14Nb.mjs +28 -0
  689. package/dist/web/assets/pdf.worker.min-D7i8o3wR.js +1 -0
  690. package/dist/web/index.html +2 -2
  691. package/package.json +6 -2
  692. package/dist/slices/article/provided-entries.js +0 -66
  693. package/dist/slices/article/run.js +0 -145
  694. package/dist/slices/images/run.js +0 -234
  695. package/dist/slices/narration/legacy-plan.js +0 -58
  696. package/dist/slices/narration/run.js +0 -270
  697. package/dist/slices/rebuild/runtime-actions.js +0 -90
  698. package/dist/slices/research/run.js +0 -224
  699. package/dist/slices/subtitles/prepare.js +0 -194
  700. package/dist/slices/subtitles/transcript.js +0 -34
  701. package/dist/slices/thumbnail/run.js +0 -219
  702. package/dist/slices/video/audio-inputs.js +0 -33
  703. package/dist/slices/video/reuse-audio.js +0 -62
  704. package/dist/slices/video/run.js +0 -78
  705. package/dist/slices/video/write-export.js +0 -158
  706. package/dist/slices/video/write-subtitles.js +0 -59
  707. package/dist/tutorials/Library-Overview.md +0 -76
  708. package/dist/tutorials/Play-Images.md +0 -161
  709. package/dist/tutorials/Play-Narration.md +0 -167
  710. package/dist/tutorials/Play-Outputs.md +0 -109
  711. package/dist/tutorials/Play-Overview.md +0 -227
  712. package/dist/tutorials/Play-Title-and-Article.md +0 -136
  713. package/dist/tutorials/Play-Video-and-Style.md +0 -143
  714. package/dist/web/assets/index-BadsoORk.css +0 -1
  715. package/dist/web/assets/index-D8LqNsCl.js +0 -326
  716. package/dist/web/assets/pdf-T7MCwjIv.js +0 -54
  717. package/dist/web/assets/pdf.worker.min-Dswkl-cV.mjs +0 -29
  718. package/dist/web/assets/pdf.worker.min-zC6nfbxG.js +0 -1
@@ -0,0 +1,74 @@
1
+ // The facts a run copies when it starts, for the keywords Slopify fills from them, and how each
2
+ // becomes a keyword's text. Browser-safe.
3
+ import { previousVideoLink } from "../youtube/placeholders.js";
4
+ // The description's own link placeholder for the next video, filled when it is shown like
5
+ // "Previous video".
6
+ export const nextVideoLink = "Next video";
7
+ // Every keyword the facts fill, for one place. A Description prompt keeps the links as the
8
+ // placeholders the description page fills when it shows the description, so they stay live.
9
+ export function factValues(facts, title, place) {
10
+ const described = place === "description";
11
+ const links = facts?.links ?? [];
12
+ const previous = facts?.previousVideo;
13
+ return {
14
+ Slopify_Title: title,
15
+ Slopify_Channel: facts?.channel ?? "",
16
+ Slopify_Series: facts?.series?.trim() ?? "",
17
+ Slopify_Links: links
18
+ .map((link) => `${link.name}: ${described ? `{{${link.name}}}` : link.url}`)
19
+ .join("\n"),
20
+ Slopify_Previous_Video: described
21
+ ? `{{${previousVideoLink}}}`
22
+ : previous === undefined
23
+ ? ""
24
+ : previous.url === undefined || previous.url === ""
25
+ ? previous.title
26
+ : `${previous.title} – ${previous.url}`,
27
+ Slopify_Next_Video: described ? `{{${nextVideoLink}}}` : (facts?.nextVideo?.title ?? ""),
28
+ Slopify_Cast: (facts?.cast ?? [])
29
+ .map((member) => member.description.trim() === "" ? member.name : `${member.name}: ${member.description}`)
30
+ .join("\n"),
31
+ Slopify_Covered_Before: (facts?.coveredBefore ?? []).join("\n"),
32
+ Slopify_Episode: facts?.episode === undefined ? "" : String(facts.episode),
33
+ Slopify_Brand: brandText(facts?.brand),
34
+ };
35
+ }
36
+ function brandText(brand) {
37
+ if (brand === undefined)
38
+ return "";
39
+ const parts = [
40
+ ...(brand.colours.length === 0 ? [] : [`Colors: ${brand.colours.join(", ")}.`]),
41
+ ...(brand.fonts.length === 0 ? [] : [`Fonts: ${brand.fonts.join(", ")}.`]),
42
+ ];
43
+ return parts.join(" ");
44
+ }
45
+ // The finished length as the description and post prompts say it: "42 min 10 s".
46
+ export function durationText(seconds) {
47
+ const total = Math.max(0, Math.round(seconds));
48
+ const hours = Math.floor(total / 3600);
49
+ const minutes = Math.floor((total % 3600) / 60);
50
+ const rest = total % 60;
51
+ return [
52
+ ...(hours > 0 ? [`${String(hours)} h`] : []),
53
+ ...(hours > 0 || minutes > 0 ? [`${String(minutes)} min`] : []),
54
+ `${String(rest)} s`,
55
+ ].join(" ");
56
+ }
57
+ // The release date as a prompt says it: "Thursday 22 October 2026", in the computer's time zone
58
+ // like every other release time Slopify shows. Blank when the video has no release time.
59
+ export function releaseDateText(at, timeZone) {
60
+ if (at === undefined || at.trim() === "")
61
+ return "";
62
+ const date = new Date(at);
63
+ if (Number.isNaN(date.getTime()))
64
+ return "";
65
+ return new Intl.DateTimeFormat("en-GB", {
66
+ weekday: "long",
67
+ day: "numeric",
68
+ month: "long",
69
+ year: "numeric",
70
+ ...(timeZone === undefined ? {} : { timeZone }),
71
+ })
72
+ .format(date)
73
+ .replace(",", "");
74
+ }
@@ -2,14 +2,35 @@
2
2
  // restated here - it is admission/substitute.ts's, imported so the editor and the run see
3
3
  // the same parse of the same body.
4
4
  import { detectSlots } from "../admission/substitute.js";
5
+ import { keywordProblems, positionOf, textEntryProblems } from "../keywords/rules.js";
5
6
  import { bodyMax, nameMax } from "./model.js";
6
- export function lintPrompt(draft) {
7
- return lint(draft.name, draft.body);
7
+ // `grandfathered` is the slot names the stored template already has: an unknown `Slopify_`
8
+ // name saved before the prefix was reserved does not stop it saving again.
9
+ export function lintPrompt(draft, grandfathered = []) {
10
+ return lint(draft.name, draft.body, draft.kind, grandfathered);
8
11
  }
9
- export function lintEntry(draft) {
10
- return lint(draft.name, draft.body);
12
+ export function lintEntry(draft, grandfathered = []) {
13
+ return lint(draft.name, draft.body, draft.category, grandfathered);
11
14
  }
12
- function lint(name, body) {
15
+ // What the editor warns about without refusing the save: a keyword where it can't be filled
16
+ // (Play refuses that run) and an old keyword name.
17
+ export function keywordWarnings(draft) {
18
+ return [
19
+ ...keywordProblems(draft.body, placeOf(draft))
20
+ .filter((problem) => problem.kind !== "unknown")
21
+ .map((problem) => ({ field: "body", message: problem.message })),
22
+ ...("category" in draft && draft.mode === "text"
23
+ ? textEntryProblems(draft.body, draft.category).map((problem) => ({
24
+ field: "body",
25
+ message: problem.message,
26
+ }))
27
+ : []),
28
+ ];
29
+ }
30
+ function placeOf(draft) {
31
+ return "kind" in draft ? draft.kind : draft.category;
32
+ }
33
+ function lint(name, body, place, grandfathered) {
13
34
  const fields = [];
14
35
  const trimmed = name.trim();
15
36
  if (trimmed === "") {
@@ -35,6 +56,9 @@ function lint(name, body) {
35
56
  for (const error of detectSlots(body).errors) {
36
57
  fields.push({ field: "body", message: describe(error, body) });
37
58
  }
59
+ for (const problem of keywordProblems(body, place, grandfathered))
60
+ if (problem.kind === "unknown")
61
+ fields.push({ field: "body", message: problem.message });
38
62
  return fields;
39
63
  }
40
64
  // The parser reports a character offset; a person editing a textarea counts lines and
@@ -50,10 +74,3 @@ function describe(error, body) {
50
74
  return `The keyword at ${where} contains a brace. Keywords cannot be placed inside other keywords.`;
51
75
  }
52
76
  }
53
- function positionOf(body, at) {
54
- const before = body.slice(0, at);
55
- const lastBreak = before.lastIndexOf("\n");
56
- const line = before.split("\n").length;
57
- const column = at - lastBreak;
58
- return `line ${String(line)}, column ${String(column)}`;
59
- }
@@ -12,6 +12,8 @@ export const promptKinds = [
12
12
  "review",
13
13
  // Speaker turns for a multi-voice run (`slices/voices/script.ts`), in place of an article.
14
14
  "script",
15
+ // A post for another site: X, LinkedIn, Instagram, TikTok, Reddit or Bluesky (`slices/social`).
16
+ "post",
15
17
  ];
16
18
  export const entryCategories = ["intro", "outro"];
17
19
  // The name is what the Play pickers show, so it shares admission's title bound.
@@ -1,4 +1,4 @@
1
- import { readSetting, writeSetting } from "../settings/repo.js";
1
+ import { deleteSetting, readJsonSetting, stringListSchema, writeSetting, } from "../settings/repo.js";
2
2
  import { promptById, promptByName } from "./repo.js";
3
3
  // An Image prompt's "Draws photorealistic pictures" tick (Library → Prompts → the prompt).
4
4
  // Pictures an image model draws in such a style can pass for real scenes, which is YouTube's
@@ -9,16 +9,8 @@ import { promptById, promptByName } from "./repo.js";
9
9
  // for when it is restored.
10
10
  export const photorealisticPromptsKey = "library.photorealisticPrompts";
11
11
  export function photorealisticPromptIds(db) {
12
- const stored = readSetting(db, photorealisticPromptsKey);
13
- if (stored === undefined)
14
- return new Set();
15
- try {
16
- const value = JSON.parse(stored);
17
- return new Set(Array.isArray(value) ? value.filter((one) => typeof one === "string") : []);
18
- }
19
- catch {
20
- return new Set();
21
- }
12
+ const value = readJsonSetting(db, photorealisticPromptsKey, stringListSchema, []);
13
+ return new Set(value);
22
14
  }
23
15
  // The prompts as the Library lists them, the ticked ones marked.
24
16
  export function withPhotorealistic(db, prompts) {
@@ -34,7 +26,7 @@ export function setPromptPhotorealistic(db, id, on) {
34
26
  const others = [...photorealisticPromptIds(db)].filter((one) => one !== id);
35
27
  const next = on ? [...others, id].sort() : others;
36
28
  if (next.length === 0)
37
- db.prepare("DELETE FROM settings WHERE key = ?").run(photorealisticPromptsKey);
29
+ deleteSetting(db, photorealisticPromptsKey);
38
30
  else
39
31
  writeSetting(db, photorealisticPromptsKey, JSON.stringify(next));
40
32
  return { ok: true };
@@ -1,8 +1,12 @@
1
1
  // Renaming a Library prompt or intro/outro. Everything names the Library by name, so a rename
2
- // rewrites the name where a future run reads it: each live template's current version (its
3
- // form and the copy it saved of the Library) and every Play draft still being edited.
4
- // Schedules run their template as it is now, so they follow. A project revision is the record
5
- // of what its run used and keeps the old name; "Used by" finds it through `formerNames`.
2
+ // rewrites the name where a future run reads it: every version of every template (its form and
3
+ // the copy it saved of the Library) and every Play draft still being edited. Every version, not
4
+ // only the newest: a gated template's schedules run its newest ready version (`template-studio/
5
+ // gate.ts`), Export pack carries that one, a version's samples stay current only while the
6
+ // versions since differ in what they changed, and a trashed template or an old version can be
7
+ // put back. The name is a pointer, so rewriting it changes nothing a version means. A project
8
+ // revision is the record of what its run used and keeps the old name; "Used by" finds it
9
+ // through `formerNames`.
6
10
  import { z } from "zod";
7
11
  import { renamedIn } from "./used-by.js";
8
12
  const documentRow = z.object({ key: z.string(), version: z.number(), document_json: z.string() });
@@ -56,9 +60,7 @@ export function renameReferences(db, ref, id, to) {
56
60
  if (ref.name === to)
57
61
  return;
58
62
  const templates = db
59
- .prepare(`SELECT t.id AS key,r.version,r.document_json FROM project_templates t
60
- JOIN project_template_revisions r ON r.template_id=t.id AND r.version=t.head_version
61
- WHERE t.deleted_at IS NULL`)
63
+ .prepare("SELECT template_id AS key,version,document_json FROM project_template_revisions")
62
64
  .all()
63
65
  .map((row) => documentRow.parse(row));
64
66
  const updateTemplate = db.prepare("UPDATE project_template_revisions SET document_json=? WHERE template_id=? AND version=?");
@@ -8,8 +8,10 @@ import { lintEntry, lintPrompt } from "./lint.js";
8
8
  import { entryCategories, promptKinds } from "./model.js";
9
9
  import { renameReferences } from "./rename.js";
10
10
  import { entryById, insertEntry, insertPrompt, promptById, replaceEntry, replacePrompt, trashEntry, trashPrompt, } from "./repo.js";
11
- export function createPrompt(deps, draft) {
12
- const fields = lintPrompt(draft);
11
+ // Duplicate names the prompt it copies (`copyOf`): a copy of one saved before the `Slopify_`
12
+ // prefix was reserved keeps the names that one already has, as an edit of it would.
13
+ export function createPrompt(deps, draft, copyOf) {
14
+ const fields = lintPrompt(draft, copyOf === undefined ? [] : (promptById(deps.db, copyOf)?.slots ?? []));
13
15
  if (fields.length > 0) {
14
16
  return { ok: false, reason: "invalid", fields };
15
17
  }
@@ -21,8 +23,10 @@ export function createPrompt(deps, draft) {
21
23
  }, prompt);
22
24
  }
23
25
  // A save replaces the row whole and keeps what was saved as a new version (`history.ts`).
24
- export function updatePrompt(deps, id, draft, restoredFrom) {
25
- const fields = lintPrompt(draft);
26
+ export function updatePrompt(deps, id, draft, restoredFrom,
27
+ // Slot names a restored version had, which it may keep like the stored prompt's.
28
+ restoredSlots = []) {
29
+ const fields = lintPrompt(draft, [...(promptById(deps.db, id)?.slots ?? []), ...restoredSlots]);
26
30
  if (fields.length > 0) {
27
31
  return { ok: false, reason: "invalid", fields };
28
32
  }
@@ -47,7 +51,7 @@ export function restorePrompt(deps, id, version) {
47
51
  const kind = promptKinds.find((one) => one === saved?.kind);
48
52
  if (saved === undefined || kind === undefined || promptById(deps.db, id) === undefined)
49
53
  return { ok: false, reason: "not-found" };
50
- return updatePrompt(deps, id, { kind, name: saved.name, body: saved.body }, version);
54
+ return updatePrompt(deps, id, { kind, name: saved.name, body: saved.body }, version, detectSlots(saved.body).names);
51
55
  }
52
56
  // Delete moves the prompt to the trash with its history; Settings → Trash puts it back or
53
57
  // removes it for good (`slices/trash`).
@@ -56,8 +60,8 @@ export function removePrompt(deps, id) {
56
60
  ? { ok: true, value: null }
57
61
  : { ok: false, reason: "not-found" };
58
62
  }
59
- export function createEntry(deps, draft) {
60
- const fields = lintEntry(draft);
63
+ export function createEntry(deps, draft, copyOf) {
64
+ const fields = lintEntry(draft, copyOf === undefined ? [] : (entryById(deps.db, copyOf)?.slots ?? []));
61
65
  if (fields.length > 0) {
62
66
  return { ok: false, reason: "invalid", fields };
63
67
  }
@@ -68,8 +72,8 @@ export function createEntry(deps, draft) {
68
72
  return true;
69
73
  }, entry);
70
74
  }
71
- export function updateEntry(deps, id, draft, restoredFrom) {
72
- const fields = lintEntry(draft);
75
+ export function updateEntry(deps, id, draft, restoredFrom, restoredSlots = []) {
76
+ const fields = lintEntry(draft, [...(entryById(deps.db, id)?.slots ?? []), ...restoredSlots]);
73
77
  if (fields.length > 0) {
74
78
  return { ok: false, reason: "invalid", fields };
75
79
  }
@@ -95,7 +99,7 @@ export function restoreEntry(deps, id, version) {
95
99
  saved.mode === null ||
96
100
  entryById(deps.db, id) === undefined)
97
101
  return { ok: false, reason: "not-found" };
98
- return updateEntry(deps, id, { category, mode: saved.mode, name: saved.name, body: saved.body }, version);
102
+ return updateEntry(deps, id, { category, mode: saved.mode, name: saved.name, body: saved.body }, version, detectSlots(saved.body).names);
99
103
  }
100
104
  export function removeEntry(deps, id) {
101
105
  return trashEntry(deps.db, id, deps.clock.now().toISOString())
@@ -4,8 +4,10 @@
4
4
  // is the one that runs.
5
5
  import { normaliseDraft, usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, } from "../admission/rules.js";
6
6
  import { collectFields, render } from "../admission/substitute.js";
7
+ import { keywordUsesOf, placedProblems, textEntryProblems, } from "../keywords/rules.js";
7
8
  import { reviewPromptKey } from "../reviews/model.js";
8
9
  import { activeReviewStages } from "../reviews/rules.js";
10
+ import { socialPlatforms } from "../social/model.js";
9
11
  import { usesScriptPrompt } from "../voices/model.js";
10
12
  import { snapshotEntry, snapshotPrompt } from "./snapshot.js";
11
13
  export function pickTemplates(db, input, snapshot) {
@@ -16,40 +18,63 @@ export function pickTemplates(db, input, snapshot) {
16
18
  // entries. A prompt left selected on a stage set to Provide asks for no field.
17
19
  const text = [];
18
20
  const image = [];
21
+ // Each picked body with the field that picked it and where it runs, for the keyword checks.
22
+ const placed = [];
23
+ const body = (kind, name, field, into, key) => {
24
+ const at = into.length;
25
+ pickBody(db, kind, name, field, missing, into, key, snapshot);
26
+ const picked = into[at];
27
+ if (picked !== undefined)
28
+ placed.push({ field, place: kind, body: picked.body });
29
+ };
19
30
  if (sources.article === "generate") {
20
- body(db, usesScriptPrompt(draft) ? "script" : "article", draft.articlePrompt, "articlePrompt", missing, text, "article", snapshot);
31
+ body(usesScriptPrompt(draft) ? "script" : "article", draft.articlePrompt, "articlePrompt", text, "article");
21
32
  }
22
33
  const intro = pickEntry(db, "intro", draft.intro, missing, snapshot);
23
34
  if (usesNarrationPreparation(draft))
24
- body(db, "narration", draft.narrationPrompt, "narrationPrompt", missing, text, "narration", snapshot);
35
+ body("narration", draft.narrationPrompt, "narrationPrompt", text, "narration");
25
36
  const outro = pickEntry(db, "outro", draft.outro, missing, snapshot);
26
37
  push(text, "intro", intro);
27
38
  push(text, "outro", outro);
39
+ if (intro !== undefined)
40
+ placed.push({ field: "intro", place: "intro", body: intro.body });
41
+ if (outro !== undefined)
42
+ placed.push({ field: "outro", place: "outro", body: outro.body });
43
+ for (const entry of [intro, outro])
44
+ if (entry?.mode === "text")
45
+ missing.push(...textEntryProblems(entry.body, entry.category));
28
46
  // None picked is the built-in prompt, which asks for no keyword.
29
47
  if (usesYoutubeDescription(draft))
30
- body(db, "description", draft.descriptionPrompt, "descriptionPrompt", missing, text, "description", snapshot);
48
+ body("description", draft.descriptionPrompt, "descriptionPrompt", text, "description");
31
49
  // The Shorts step's two prompts; none picked is the built-in one, which asks for no keyword.
32
50
  if (usesShorts(draft)) {
33
- body(db, "shorts", draft.shorts?.prompt, "shorts.prompt", missing, text, "shorts", snapshot);
34
- body(db, "image", draft.shorts?.imagePrompt, "shorts.imagePrompt", missing, image, "shortsImage", snapshot);
51
+ body("shorts", draft.shorts?.prompt, "shorts.prompt", text, "shorts");
52
+ body("image", draft.shorts?.imagePrompt, "shorts.imagePrompt", image, "shortsImage");
35
53
  }
36
54
  // Each reviewed stage's Review prompt; none picked is the stage's built-in one.
37
55
  for (const stage of activeReviewStages(draft))
38
- body(db, "review", draft.reviews?.stages[stage]?.prompt, `reviews.stages.${stage}.prompt`, missing, text, reviewPromptKey(stage), snapshot);
56
+ body("review", draft.reviews?.stages[stage]?.prompt, `reviews.stages.${stage}.prompt`, text, reviewPromptKey(stage));
57
+ // Each site's post prompt, for the sites whose post is written by one; none picked is the
58
+ // built-in one, which asks for no keyword.
59
+ for (const platform of socialPlatforms) {
60
+ const choice = draft.social?.[platform];
61
+ if (choice?.text === "prompt")
62
+ body("post", choice.prompt, `social.${platform}.prompt`, text, `social.${platform}`);
63
+ }
39
64
  // The establishing image's prompt: an image prompt like the others, its keywords filled the
40
65
  // same way.
41
66
  if (usesReference(draft) && draft.reference?.source === "prompt")
42
- body(db, "image", draft.reference.prompt, "reference.prompt", missing, image, "referencePrompt", snapshot);
67
+ body("image", draft.reference.prompt, "reference.prompt", image, "referencePrompt");
43
68
  if (sources.images === "generate") {
44
69
  for (const [index, picked] of draft.imagePrompts.entries()) {
45
70
  const field = `imagePrompts.${String(index)}.name`;
46
- body(db, "image", picked.name, field, missing, image, `imagePrompts.${String(index)}`, snapshot);
71
+ body("image", picked.name, field, image, `imagePrompts.${String(index)}`);
47
72
  }
48
73
  }
49
74
  // Both Generate modes read the same thumbnail template; Prompt by LLM sends it as the
50
75
  // instruction rather than to the image provider.
51
76
  if (sources.thumbnail === "from_prompt" || sources.thumbnail === "prompt_by_llm") {
52
- body(db, "thumbnail", draft.thumbnailPrompt, "thumbnailPrompt", missing, image, "thumbnailPrompt", snapshot);
77
+ body("thumbnail", draft.thumbnailPrompt, "thumbnailPrompt", image, "thumbnailPrompt");
53
78
  }
54
79
  const fields = collectFields(
55
80
  // The title's keywords are asked for like a prompt's: it is filled when the run starts.
@@ -58,7 +83,8 @@ export function pickTemplates(db, input, snapshot) {
58
83
  draft: withSavedModes(draft, intro, outro),
59
84
  requiredSlots: fields.map((field) => field.name),
60
85
  bodies: [...text, ...image],
61
- missing,
86
+ missing: [...missing, ...placedProblems(placed, draft.values)],
87
+ keywordUses: keywordUsesOf(placed),
62
88
  };
63
89
  }
64
90
  // Every picked body rendered once, with the trimmed values
@@ -70,7 +96,7 @@ export function renderPicked(picked, values) {
70
96
  }
71
97
  return rendered;
72
98
  }
73
- function body(db, kind, name, field, missing, into, key, snapshot) {
99
+ function pickBody(db, kind, name, field, missing, into, key, snapshot) {
74
100
  // An unpicked prompt is admission's rule to state ("Pick an article prompt."), so this
75
101
  // says nothing about it and the form marks the field once.
76
102
  if (name === undefined || name.trim() === "") {
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { errorText } from "../../kernel/errors.js";
2
3
  import { documentThemeNameMax } from "../document/model.js";
3
4
  import { documentThemeSchema } from "../document/theme-schema.js";
4
5
  import { narrationAliasSchema } from "../narration/aliases-schema.js";
@@ -16,7 +17,7 @@ const itemsMax = 10_000;
16
17
  const sectionWords = {
17
18
  prompts: "prompts",
18
19
  entries: "intros or outros",
19
- documentThemes: "PDF themes",
20
+ documentThemes: "PDF looks",
20
21
  aliases: "narration aliases",
21
22
  };
22
23
  // Only what the editor saves travels: ids, keywords and dates are the install's own.
@@ -92,7 +93,7 @@ export function readLibraryFile(text, section) {
92
93
  if (!top.success)
93
94
  return {
94
95
  ok: false,
95
- message: `it isn't a Slopify library file. Pick a .json file made by Export on a Library tab.`,
96
+ message: `it isn't a Slopify library file. Pick a .json file made by Export on Prompts, the PDF looks or Pronunciation.`,
96
97
  };
97
98
  if (top.data.version > libraryFileVersion)
98
99
  return {
@@ -103,7 +104,7 @@ export function readLibraryFile(text, section) {
103
104
  if (!Array.isArray(list) || list.length === 0)
104
105
  return {
105
106
  ok: false,
106
- message: `it holds no ${what}. Import it on the Library tab it was exported from.`,
107
+ message: `it holds no ${what}. Import it on the page it was exported from.`,
107
108
  };
108
109
  if (list.length > itemsMax)
109
110
  return {
@@ -202,7 +203,7 @@ export async function importEach(items, options) {
202
203
  break;
203
204
  }
204
205
  catch (error) {
205
- reason = error instanceof Error ? error.message : String(error);
206
+ reason = errorText(error);
206
207
  break;
207
208
  }
208
209
  }
@@ -5,6 +5,8 @@
5
5
  // `snapshot.ts` use.
6
6
  import { z } from "zod";
7
7
  import { liveProject } from "../admission/repo.js";
8
+ import { runnableVersion } from "../template-studio/gate.js";
9
+ import { gatesOf, notTemplateSample } from "../template-studio/repo.js";
8
10
  function record(value) {
9
11
  return typeof value === "object" && value !== null && !Array.isArray(value)
10
12
  ? value
@@ -40,6 +42,8 @@ function slotsIn(value, ref) {
40
42
  return field(shorts, "prompt");
41
43
  case "review":
42
44
  return Object.values(record(record(value.reviews)?.stages) ?? {}).flatMap((stage) => field(record(stage), "prompt"));
45
+ case "post":
46
+ return Object.values(record(value.social) ?? {}).flatMap((one) => field(record(one), "prompt"));
43
47
  case "image":
44
48
  return [
45
49
  ...(Array.isArray(value.imagePrompts) ? value.imagePrompts : []).flatMap((one) => field(record(one), "name")),
@@ -106,18 +110,28 @@ export function usedBy(db, ref, formerNames = []) {
106
110
  const parsed = templateRow.parse(row);
107
111
  return matches(formOf(parsed.document_json)) ? [{ id: parsed.id, name: parsed.name }] : [];
108
112
  });
109
- // A schedule runs its template as it is now (`schedules/scheduler.ts`), so the template's
110
- // current version is what it uses.
113
+ // A schedule runs the version of its template the studio gate lets it run
114
+ // (`template-studio/schedule-gate.ts`), which can be older than the newest one.
115
+ const runsOf = new Map();
116
+ const formRun = (templateId, head) => {
117
+ if (!runsOf.has(templateId)) {
118
+ const version = runnableVersion(gatesOf(db, templateId), head) ?? head;
119
+ const row = db
120
+ .prepare("SELECT document_json FROM project_template_revisions WHERE template_id=? AND version=?")
121
+ .get(templateId, version);
122
+ runsOf.set(templateId, row === undefined ? undefined : formOf(String(row.document_json)));
123
+ }
124
+ return runsOf.get(templateId);
125
+ };
111
126
  const schedules = db
112
- .prepare(`SELECT s.id,s.name,s.status,r.document_json FROM schedules s
127
+ .prepare(`SELECT s.id,s.name,s.status,s.template_id,t.head_version FROM schedules s
113
128
  JOIN project_templates t ON t.id=s.template_id
114
- JOIN project_template_revisions r ON r.template_id=t.id AND r.version=t.head_version
115
- WHERE s.deleted_at IS NULL AND s.status IN ('active','paused') AND ${mentions("r.document_json")}
129
+ WHERE s.deleted_at IS NULL AND s.status IN ('active','paused') AND s.template_kind='project'
116
130
  ORDER BY lower(s.name),s.id`)
117
- .all(...plain)
131
+ .all()
118
132
  .flatMap((row) => {
119
133
  const parsed = scheduleRow.parse(row);
120
- return matches(formOf(parsed.document_json))
134
+ return matches(formRun(parsed.template_id, parsed.head_version))
121
135
  ? [{ id: parsed.id, name: parsed.name, status: parsed.status }]
122
136
  : [];
123
137
  });
@@ -126,7 +140,7 @@ export function usedBy(db, ref, formerNames = []) {
126
140
  (SELECT count(*) FROM project_revisions x WHERE x.project_id=r.project_id) AS total
127
141
  FROM project_revisions r JOIN projects p ON p.id=r.project_id
128
142
  LEFT JOIN project_heads h ON h.project_id=r.project_id
129
- WHERE ${liveProject("p")} AND ${mentions("r.config")}
143
+ WHERE ${liveProject("p")} AND ${notTemplateSample("p")} AND ${mentions("r.config")}
130
144
  ORDER BY p.created_at DESC,p.id,r.created_at`)
131
145
  .all(...plain)
132
146
  .map((row) => revisionRow.parse(row));
@@ -146,7 +160,7 @@ export function usedBy(db, ref, formerNames = []) {
146
160
  // A project from before revisions existed has none yet; its own config is what it ran with.
147
161
  for (const row of db
148
162
  .prepare(`SELECT p.id,p.title,p.config FROM projects p
149
- WHERE ${liveProject("p")}
163
+ WHERE ${liveProject("p")} AND ${notTemplateSample("p")}
150
164
  AND NOT EXISTS (SELECT 1 FROM project_revisions r WHERE r.project_id=p.id)
151
165
  AND ${mentions("p.config")} ORDER BY p.created_at DESC,p.id`)
152
166
  .all(...plain)
@@ -167,7 +181,8 @@ const scheduleRow = z.object({
167
181
  id: z.string(),
168
182
  name: z.string(),
169
183
  status: z.string(),
170
- document_json: z.string(),
184
+ template_id: z.string(),
185
+ head_version: z.number(),
171
186
  });
172
187
  const revisionRow = z.object({
173
188
  project_id: z.string(),
@@ -1,5 +1,6 @@
1
- import { renameSync } from "node:fs";
1
+ import { renameSync, rmSync } from "node:fs";
2
2
  import { z } from "zod";
3
+ import { editSettings } from "../../kernel/ports/settings-places.js";
3
4
  import { runFfmpeg } from "../video/ffmpeg.js";
4
5
  // ceiling: below -70 LUFS (the filter's absolute gate) or under 0.4 s (one measuring block)
5
6
  // there is nothing to measure: a piece that quiet or short is left as it is.
@@ -89,6 +90,7 @@ export async function measure(run, input, goal, chain = "") {
89
90
  args: measureArgs(input, goal, chain),
90
91
  signal: run.signal,
91
92
  log: run.log,
93
+ priority: run.priority,
92
94
  onProgress: () => { },
93
95
  onStderr: (text) => {
94
96
  stderr += text;
@@ -96,7 +98,7 @@ export async function measure(run, input, goal, chain = "") {
96
98
  });
97
99
  const measured = parseMeasured(stderr);
98
100
  if (measured === undefined)
99
- throw new Error("Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in Edit project → Pauses and volume, then use Download diagnostics in Settings and report it.");
101
+ throw new Error(`Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in ${editSettings.sound}, then use Download diagnostics in Settings and report it.`);
100
102
  return measured;
101
103
  }
102
104
  export async function measureFile(run, path, goal) {
@@ -109,8 +111,15 @@ export async function normalizeFile(run, input, output, goal, format) {
109
111
  // The channels are set before anything is measured: a stereo file mixed down to mono is
110
112
  // louder per channel (the mix keeps its loudness, so each sample is about 3 dB higher), and
111
113
  // the gain and the peak ceiling have to hold for what is written.
112
- const channels = `aformat=channel_layouts=${format.channels === 1 ? "mono" : "stereo"}`;
113
- const measured = await measure(run, ["-i", input], goal, channels);
114
+ const measured = await measure(run, ["-i", input], goal, channelsOf(format));
115
+ await writeLevelled(run, input, output, goal, measured, format);
116
+ return measured;
117
+ }
118
+ function channelsOf(format) {
119
+ return `aformat=channel_layouts=${format.channels === 1 ? "mono" : "stereo"}`;
120
+ }
121
+ // `input` written to `output` with the gain that takes `measured` to `goal` (`gainFilter`).
122
+ async function writeLevelled(run, input, output, goal, measured, format) {
114
123
  await runFfmpeg({
115
124
  bin: run.bin,
116
125
  args: [
@@ -125,7 +134,7 @@ export async function normalizeFile(run, input, output, goal, format) {
125
134
  "-map",
126
135
  "0:a:0",
127
136
  "-af",
128
- `${channels},${gainFilter(goal, measured, format.sampleRate)}`,
137
+ `${channelsOf(format)},${gainFilter(goal, measured, format.sampleRate)}`,
129
138
  "-c:a",
130
139
  "pcm_f32le",
131
140
  "-f",
@@ -134,23 +143,55 @@ export async function normalizeFile(run, input, output, goal, format) {
134
143
  ],
135
144
  signal: run.signal,
136
145
  log: run.log,
146
+ priority: run.priority,
137
147
  onProgress: () => { },
138
148
  });
139
- return measured;
140
149
  }
141
150
  // ceiling: within a fifth of a LU of the level no one hears the difference.
142
151
  const correctAbove = 0.2;
143
- // One file brought to `goal` (`normalizeFile`), measured again, and corrected once when the
144
- // limiter left it more than a fifth of a LU under the level. Returns the first measurement and
145
- // the last.
152
+ // ceiling: two corrections land a voice whose peaks stand 17 dB over its loudness (raw TTS)
153
+ // within a few tenths of a LU; a third would only chase the last tenth.
154
+ const corrections = 2;
155
+ // ceiling: the most gain a correction adds over the plain gain. Past it the limiter would
156
+ // flatten the voice; the file lands short instead, and its report says how far.
157
+ const extraGainMax = 6;
158
+ // One file brought to `goal` (`normalizeFile`), measured again, and corrected when the limiter
159
+ // left it more than a fifth of a LU under the level. Each correction writes the input again
160
+ // with more gain (so the limiter holds the peaks once, never twice), the gain found from the
161
+ // levels the earlier gains gave. Returns the first measurement and the last.
146
162
  export async function levelFile(run, input, output, goal, format) {
147
163
  const before = await normalizeFile(run, input, output, goal, format);
148
164
  let after = await measureFile(run, output, goal);
149
- if (measurable(after) && Math.abs(after.integrated - goal.lufs) > correctAbove) {
165
+ if (!measurable(before))
166
+ return { before, after };
167
+ const plain = goal.lufs - before.integrated;
168
+ const tried = [
169
+ { gain: plain, lufs: after.integrated },
170
+ ];
171
+ for (let pass = 0; pass < corrections; pass += 1) {
172
+ if (!measurable(after) || Math.abs(after.integrated - goal.lufs) <= correctAbove)
173
+ break;
174
+ const last = tried[tried.length - 1];
175
+ const prior = tried[tried.length - 2];
176
+ if (last === undefined)
177
+ break;
178
+ const slope = prior === undefined || last.lufs === prior.lufs
179
+ ? 1
180
+ : (last.lufs - prior.lufs) / (last.gain - prior.gain);
181
+ const gain = Math.min(plain + extraGainMax, last.gain + (goal.lufs - last.lufs) / (slope > 0.05 ? slope : 1));
182
+ if (Math.abs(gain - last.gain) < 0.05)
183
+ break;
150
184
  const corrected = `${output}.corrected.wav`;
151
- await normalizeFile(run, output, corrected, goal, format);
185
+ await writeLevelled(run, input, corrected, { ...goal, lufs: before.integrated + gain }, before, format);
186
+ const next = await measureFile(run, corrected, goal);
187
+ // A correction that landed further off than the file it would replace is dropped.
188
+ if (Math.abs(next.integrated - goal.lufs) >= Math.abs(after.integrated - goal.lufs)) {
189
+ rmSync(corrected, { force: true });
190
+ break;
191
+ }
152
192
  renameSync(corrected, output);
153
- after = await measureFile(run, output, goal);
193
+ after = next;
194
+ tried.push({ gain, lufs: next.integrated });
154
195
  }
155
196
  return { before, after };
156
197
  }
@@ -9,10 +9,11 @@
9
9
  // The ambient bed and the shorts' music are mixed before the master and ducked by the levelled
10
10
  // voice, so they keep their place under it.
11
11
  //
12
- // Browser-safe: Play, Edit project and Settings convert the same numbers.
12
+ // Browser-safe: Create, Edit settings and Settings convert the same numbers.
13
13
  //
14
14
  // A run without the setting has no `loudness` at all, which is what every project saved before
15
15
  // it was, and every recipe leaves it out of its fingerprint then.
16
+ import { createRows, editSettings } from "../../kernel/ports/settings-places.js";
16
17
  // YouTube and Spotify play everything back at about -14 LUFS, so a video mastered there is
17
18
  // neither turned down nor left quiet. Audiobook shops ask for about -18 to -20 LUFS with more
18
19
  // headroom, so the listening files sit lower.
@@ -87,7 +88,7 @@ export function loudnessProblem(lufs, target) {
87
88
  db >= loudnessDbMin - 1e-9 &&
88
89
  db <= loudnessDbMax + 1e-9
89
90
  ? undefined
90
- : `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (Play → Video and style, Edit project → Pauses and volume, or Settings → General).`;
91
+ : `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (${createRows.video}, ${editSettings.sound}, or Settings → Production defaults).`;
91
92
  }
92
93
  export function loudnessFields(settings) {
93
94
  if (settings === undefined)