@gentbajko/slopify 2.5.0 → 3.0.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 (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,7 +1,8 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { z } from "zod";
2
3
  import { transact } from "../../kernel/db/tx.js";
3
4
  import { derive } from "../../kernel/runner/graph.js";
4
- import { projectPaused, stagesOf } from "../admission/repo.js";
5
+ import { projectExists, projectPaused, stagesOf } from "../admission/repo.js";
5
6
  import { readStartReceipt } from "../play-drafts/start-repo.js";
6
7
  import { scheduleRunSchema, scheduleSummarySchema, } from "./schema.js";
7
8
  const rowSchema = z.object({
@@ -23,30 +24,39 @@ const rowSchema = z.object({
23
24
  created_at: z.string(),
24
25
  updated_at: z.string(),
25
26
  deleted_at: z.string().nullable(),
27
+ brief: z.string().nullable(),
28
+ topic_mode: z.string(),
29
+ topic_min: z.number(),
30
+ topic_llm_json: z.string().nullable(),
31
+ topics_generating_at: z.string().nullable(),
32
+ topics_generated_at: z.string().nullable(),
33
+ topics_failed_at: z.string().nullable(),
34
+ topics_error: z.string().nullable(),
35
+ held_topics: z.number(),
26
36
  });
37
+ const scheduleColumns = `id,name,template_id,template_version,cadence_json,timezone,missed_policy,
38
+ overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,
39
+ created_at,updated_at,deleted_at,brief,topic_mode,topic_min,topic_llm_json,topics_generating_at,
40
+ topics_generated_at,topics_failed_at,topics_error,
41
+ (SELECT count(*) FROM schedule_topics AS held
42
+ WHERE held.schedule_id=schedules.id AND held.state='held') AS held_topics`;
27
43
  export function scheduleById(db, id) {
28
44
  const row = db
29
- .prepare(`SELECT id,name,template_id,template_version,cadence_json,timezone,missed_policy,
30
- overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,created_at,updated_at,
31
- deleted_at
45
+ .prepare(`SELECT ${scheduleColumns}
32
46
  FROM schedules WHERE id=?`)
33
47
  .get(id);
34
48
  return row === undefined ? undefined : parseSchedule(row);
35
49
  }
36
50
  export function scheduleRows(db) {
37
51
  return db
38
- .prepare(`SELECT id,name,template_id,template_version,cadence_json,timezone,missed_policy,
39
- overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,created_at,updated_at,
40
- deleted_at
52
+ .prepare(`SELECT ${scheduleColumns}
41
53
  FROM schedules ORDER BY created_at DESC,id`)
42
54
  .all()
43
55
  .map(parseSchedule);
44
56
  }
45
57
  export function dueSchedules(db, now) {
46
58
  return db
47
- .prepare(`SELECT id,name,template_id,template_version,cadence_json,timezone,missed_policy,
48
- overlap_policy,spend_limit_cents,items_json,topic_keyword,values_json,status,version,next_run_at,created_at,updated_at,
49
- deleted_at
59
+ .prepare(`SELECT ${scheduleColumns}
50
60
  FROM schedules WHERE deleted_at IS NULL AND status='active' AND next_run_at IS NOT NULL
51
61
  AND next_run_at <= ?
52
62
  ORDER BY next_run_at,id`)
@@ -57,17 +67,27 @@ export function insertSchedule(db, schedule, creationHash) {
57
67
  db.prepare(`INSERT INTO schedules
58
68
  (id,name,template_id,template_version,cadence_json,timezone,missed_policy,overlap_policy,
59
69
  spend_limit_cents,items_json,topic_keyword,values_json,status,version,creation_hash,
60
- next_run_at,created_at,updated_at,deleted_at)
61
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(schedule.id, schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, creationHash, schedule.nextRunAt, schedule.createdAt, schedule.updatedAt, schedule.deletedAt);
70
+ next_run_at,created_at,updated_at,deleted_at,brief,topic_mode,topic_min,topic_llm_json)
71
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(schedule.id, schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, creationHash, schedule.nextRunAt, schedule.createdAt, schedule.updatedAt, schedule.deletedAt, ...generationColumns(schedule));
72
+ }
73
+ function generationColumns(schedule) {
74
+ const generation = schedule.topicGeneration;
75
+ return [
76
+ schedule.brief,
77
+ generation.mode,
78
+ generation.keepAtLeast,
79
+ generation.llm === null ? null : JSON.stringify(generation.llm),
80
+ ];
62
81
  }
63
82
  export function updateScheduleRow(db, schedule, mutationId, mutationHash, baseVersion) {
64
83
  const changed = db
65
84
  .prepare(`UPDATE schedules SET name=?,template_id=?,template_version=?,cadence_json=?,timezone=?,
66
85
  missed_policy=?,overlap_policy=?,spend_limit_cents=?,items_json=?,topic_keyword=?,
67
86
  values_json=?,status=?,version=?,
68
- next_run_at=?,updated_at=?,mutation_id=?,mutation_hash=?
87
+ next_run_at=?,updated_at=?,mutation_id=?,mutation_hash=?,
88
+ brief=?,topic_mode=?,topic_min=?,topic_llm_json=?,topics_failed_at=NULL,topics_error=NULL
69
89
  WHERE id=? AND version=? AND deleted_at IS NULL`)
70
- .run(schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, schedule.nextRunAt, schedule.updatedAt, mutationId, mutationHash, schedule.id, baseVersion);
90
+ .run(schedule.name, schedule.templateId, schedule.templateVersion, JSON.stringify(schedule.cadence), schedule.timezone, schedule.missedPolicy, schedule.overlapPolicy, schedule.spendLimitCents, JSON.stringify(schedule.items), schedule.topicKeyword, JSON.stringify(schedule.values), schedule.status, schedule.version, schedule.nextRunAt, schedule.updatedAt, mutationId, mutationHash, ...generationColumns(schedule), schedule.id, baseVersion);
71
91
  return Number(changed.changes) === 1;
72
92
  }
73
93
  export function tombstoneScheduleRow(db, id, version, deletedAt) {
@@ -138,7 +158,13 @@ function consumeTopic(db, scheduleId, used, updatedAt) {
138
158
  if (at === -1)
139
159
  return;
140
160
  const items = schedule.items.filter((_item, index) => index !== at);
141
- const done = items.length === 0 && (schedule.status === "active" || schedule.status === "paused");
161
+ // A schedule that finds its own topics keeps going when its queue empties.
162
+ const done = items.length === 0 &&
163
+ schedule.topicGeneration.mode === "off" &&
164
+ (schedule.status === "active" || schedule.status === "paused");
165
+ // Remembered so topic generation never suggests it again.
166
+ db.prepare(`INSERT INTO schedule_topics (id,schedule_id,title,state,rank,created_at,decided_at)
167
+ VALUES (?,?,?,'used',0,?,?)`).run(randomUUID(), scheduleId, used.title, updatedAt, updatedAt);
142
168
  db.prepare(`UPDATE schedules SET items_json=?,status=?,next_run_at=?,version=version+1,updated_at=?
143
169
  WHERE id=? AND deleted_at IS NULL`).run(JSON.stringify(items), done ? "completed" : schedule.status, done ? null : schedule.nextRunAt, updatedAt, scheduleId);
144
170
  }
@@ -217,10 +243,11 @@ export function settleTerminalScheduleRuns(db, settledAt) {
217
243
  });
218
244
  }
219
245
  function projectIsActive(db, projectId) {
220
- if (db.prepare("SELECT 1 FROM projects WHERE id=?").get(projectId) === undefined)
246
+ // A project in the trash takes no part in a schedule's overlap: it cannot run there.
247
+ if (!projectExists(db, projectId))
221
248
  return false;
222
249
  const state = derive(stagesOf(db, projectId), projectPaused(db, projectId));
223
- return state !== "done" && state !== "failed" && state !== "canceled";
250
+ return state !== "done" && state !== "partial" && state !== "failed" && state !== "canceled";
224
251
  }
225
252
  export function recoverRunningRuns(db, endedAt, error = "Slopify was closed while this run was in progress. Check its video in Projects; the next run will go ahead as planned.") {
226
253
  return transact(db, () => {
@@ -267,6 +294,19 @@ function parseSchedule(row) {
267
294
  items: JSON.parse(value.items_json),
268
295
  topicKeyword: value.topic_keyword,
269
296
  values: JSON.parse(value.values_json),
297
+ brief: value.brief,
298
+ topicGeneration: {
299
+ mode: value.topic_mode,
300
+ keepAtLeast: value.topic_min,
301
+ llm: value.topic_llm_json === null ? null : JSON.parse(value.topic_llm_json),
302
+ },
303
+ topics: {
304
+ held: value.held_topics,
305
+ generatingSince: value.topics_generating_at,
306
+ generatedAt: value.topics_generated_at,
307
+ failedAt: value.topics_failed_at,
308
+ error: value.topics_error,
309
+ },
270
310
  status: value.status,
271
311
  version: value.version,
272
312
  nextRunAt: value.next_run_at,
@@ -308,7 +348,7 @@ function parseRun(row) {
308
348
  const internalRunError = "Slopify hit an internal error while starting this run. The next run will try again; if it keeps happening, use Download diagnostics in Settings and report it.";
309
349
  const runErrors = {
310
350
  "missing-template": "The template this schedule uses was deleted or changed. Edit the schedule and choose a template again.",
311
- "unsupported-media": "This template uses audio, images, a thumbnail or shorts background music you supplied, which scheduled runs cannot use. Pick a template that generates these instead, or remove the music under Outputs → Export → More shorts options on Play and save the template again.",
351
+ "unsupported-media": "This template uses audio, images, a thumbnail, shorts background music or an ambient sound file you supplied, which scheduled runs cannot use. Pick a template that generates these instead, or on Play remove the music under Outputs → Export → More shorts options (or pick Rain, Fireplace or Wind under Outputs → Export → Ambient sound) and save the template again.",
312
352
  "spend-limit": "Not started: the estimated cost was above this schedule's spend limit, or some prices were unknown. Raise the spend limit or choose models with known prices.",
313
353
  readiness: "Not started: a provider was not ready, for example a missing API key, model or voice. Check Settings → Providers; the next run will try again.",
314
354
  "not-found": "The template's settings are no longer valid, for example a prompt or voice it uses was deleted. Fix the template in Library > Templates and save it.",
@@ -1,21 +1,62 @@
1
1
  import { transact } from "../../kernel/db/tx.js";
2
- import { render } from "../admission/substitute.js";
3
2
  import { reviewDraft } from "../play-drafts/review.js";
4
3
  import { createDraft } from "../play-drafts/service.js";
5
4
  import { startPlayDraft } from "../play-drafts/start.js";
6
5
  import { freshTemplateDraft } from "../project-templates/setup.js";
7
6
  import { nextOccurrence } from "./calendar.js";
8
- import { activeRun, dueSchedules, insertRun, recordRunDispatch, recordRunStartIdentity, recoverRunningRuns, scheduleById, updateRun, } from "./repo.js";
7
+ import { activeRun, dueSchedules, insertRun, recordRunDispatch, recordRunStartIdentity, recoverRunningRuns, scheduleById, scheduleRows, updateRun, } from "./repo.js";
8
+ import { renderedTitle, scheduledValues } from "./topic-list.js";
9
+ import { generateTopics, generationDue, releaseTopicLeases } from "./topics.js";
9
10
  const missedGraceMs = 60_000;
10
11
  export function createScheduleRunner(deps) {
11
12
  let ticking = false;
13
+ // ceiling: one entry per schedule generating now.
14
+ const generations = new Set();
15
+ const stopping = new AbortController();
16
+ const generated = async () => {
17
+ while (generations.size > 0)
18
+ await Promise.allSettled([...generations]);
19
+ };
20
+ // Topic generation can take minutes, so a tick starts it and moves on: another schedule's
21
+ // run is never late because of it. The lease inside `generateTopics` is what keeps it to
22
+ // one at a time per schedule.
23
+ const start = (scheduleId, force) => {
24
+ if (stopping.signal.aborted)
25
+ return;
26
+ const running = generateTopics(deps, scheduleId, { signal: stopping.signal, force })
27
+ .then((result) => {
28
+ if (!result.ok && result.reason === "failed")
29
+ deps.log.write("warn", "schedule.topics", { detail: result.message });
30
+ })
31
+ .catch(() => {
32
+ deps.log.write("error", "schedule.topics", {
33
+ detail: "Topic generation could not record its result.",
34
+ });
35
+ })
36
+ .finally(() => {
37
+ generations.delete(running);
38
+ });
39
+ generations.add(running);
40
+ };
41
+ const startGenerations = (now) => {
42
+ for (const schedule of scheduleRows(deps.db))
43
+ if (generationDue(schedule, now))
44
+ start(schedule.id, false);
45
+ };
12
46
  return {
47
+ generated,
48
+ requestTopics: (scheduleId) => start(scheduleId, true),
49
+ stop: async () => {
50
+ stopping.abort();
51
+ await generated();
52
+ },
13
53
  tick: async (now = deps.clock.now()) => {
14
54
  if (ticking)
15
55
  return;
16
56
  ticking = true;
17
57
  try {
18
58
  recoverRunningRuns(deps.db, now.toISOString(), "The previous schedule tick could not record this run's result.");
59
+ startGenerations(now);
19
60
  const due = dueSchedules(deps.db, now.toISOString());
20
61
  const claimed = due.flatMap((schedule) => claim(deps, schedule, now));
21
62
  const settled = await Promise.allSettled(claimed.map((entry) => execute(deps, entry)));
@@ -27,7 +68,10 @@ export function createScheduleRunner(deps) {
27
68
  ticking = false;
28
69
  }
29
70
  },
30
- recover: (now = deps.clock.now()) => recoverRunningRuns(deps.db, now.toISOString()),
71
+ recover: (now = deps.clock.now()) => {
72
+ releaseTopicLeases(deps);
73
+ return recoverRunningRuns(deps.db, now.toISOString());
74
+ },
31
75
  };
32
76
  }
33
77
  function claim(deps, schedule, now) {
@@ -47,7 +91,11 @@ function claim(deps, schedule, now) {
47
91
  current.status !== "active")
48
92
  return [];
49
93
  const overlapping = activeRun(deps.db, schedule.id, now.toISOString());
50
- const skip = occurrence || overlapping;
94
+ // A schedule that finds its own topics never runs the template without one.
95
+ const noTopic = current.topicGeneration.mode !== "off" && current.items.length === 0
96
+ ? noTopicReason(current)
97
+ : null;
98
+ const skip = occurrence || overlapping || noTopic !== null;
51
99
  const run = {
52
100
  id: deps.uuid(),
53
101
  scheduleId: schedule.id,
@@ -61,7 +109,9 @@ function claim(deps, schedule, now) {
61
109
  error: skip
62
110
  ? overlapping
63
111
  ? "Skipped because the previous run of this schedule was still going."
64
- : "Skipped because Slopify was not running at the scheduled time."
112
+ : occurrence
113
+ ? "Skipped because Slopify was not running at the scheduled time."
114
+ : noTopic
65
115
  : null,
66
116
  };
67
117
  if (!insertRun(deps.db, run))
@@ -72,6 +122,14 @@ function claim(deps, schedule, now) {
72
122
  return skip ? [] : [{ run, schedule }];
73
123
  });
74
124
  }
125
+ function noTopicReason(schedule) {
126
+ const { held, error } = schedule.topics;
127
+ if (held > 0)
128
+ return `Skipped because no topic was approved: ${String(held)} ${held === 1 ? "topic is" : "topics are"} waiting for you. Approve them under Schedules → ${schedule.name} → Topics waiting.`;
129
+ if (error !== null)
130
+ return `Skipped because the topic queue was empty. ${error}`;
131
+ return "Skipped because the topic queue was empty and new topics were still being generated. The next run uses the first one.";
132
+ }
75
133
  async function execute(deps, claimed) {
76
134
  let run = claimed.run;
77
135
  try {
@@ -139,18 +197,11 @@ async function execute(deps, claimed) {
139
197
  // same keywords, so "D&D Lore: {{Topic}}" names the project after the topic. A topic saved
140
198
  // before topics had a keyword brings its own title and values instead.
141
199
  function runForm(form, schedule, topic) {
142
- const values = {
143
- ...form.values,
144
- ...schedule.values,
145
- ...(topic?.values ?? {}),
146
- ...(topic !== undefined && schedule.topicKeyword !== null
147
- ? { [schedule.topicKeyword]: topic.title }
148
- : {}),
200
+ return {
201
+ ...form,
202
+ title: renderedTitle(form, schedule, topic),
203
+ values: scheduledValues(form, schedule, topic),
149
204
  };
150
- const title = topic !== undefined && schedule.topicKeyword === null
151
- ? topic.title
152
- : render(form.title, values);
153
- return { ...form, title, values };
154
205
  }
155
206
  class ScheduleDispatchError extends Error {
156
207
  reason;
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { projectStates } from "../../kernel/pipeline.js";
3
+ import { thinkingModes } from "../../kernel/ports/llm.js";
2
4
  import { cadenceSchema, validTimeZone } from "./calendar.js";
3
5
  const id = z.uuid();
4
6
  const keywordName = z
@@ -9,14 +11,58 @@ const keywordName = z
9
11
  const values = z.record(keywordName, z.string().max(10000)).readonly();
10
12
  // A queued topic. Each run takes the first one and removes it once its project starts.
11
13
  // `values` is only filled on schedules saved before topics had one keyword.
14
+ export const topicTitleSchema = z.string().trim().min(1).max(200);
12
15
  const item = z
13
16
  .object({
14
- title: z.string().trim().min(1).max(200),
17
+ title: topicTitleSchema,
15
18
  values,
16
19
  })
17
20
  .strict()
18
21
  .readonly();
19
22
  const policy = z.enum(["skip", "run-once"]);
23
+ export const topicModes = ["off", "queue", "hold"];
24
+ export const topicMinDefault = 10;
25
+ export const briefMax = 4000;
26
+ // Whether a schedule asks an LLM for topics when its queue runs low, and whether they join the
27
+ // queue at once or wait for approval. `llm` is null for the template's own LLM.
28
+ export const topicGenerationSchema = z
29
+ .object({
30
+ mode: z.enum(topicModes),
31
+ keepAtLeast: z.number().int().min(1).max(100),
32
+ llm: z
33
+ .object({
34
+ provider: z.string().min(1).max(100),
35
+ model: z.string().min(1).max(200),
36
+ thinking: z.enum(thinkingModes).optional(),
37
+ })
38
+ .strict()
39
+ .readonly()
40
+ .nullable(),
41
+ })
42
+ .strict()
43
+ .readonly();
44
+ export const topicGenerationOff = {
45
+ mode: "off",
46
+ keepAtLeast: topicMinDefault,
47
+ llm: null,
48
+ };
49
+ const topicState = z
50
+ .object({
51
+ held: z.number().int().nonnegative(),
52
+ generatingSince: z.string().nullable(),
53
+ generatedAt: z.string().nullable(),
54
+ failedAt: z.string().nullable(),
55
+ error: z.string().nullable(),
56
+ })
57
+ .strict()
58
+ .readonly();
59
+ export const topicStateIdle = {
60
+ held: 0,
61
+ generatingSince: null,
62
+ generatedAt: null,
63
+ failedAt: null,
64
+ error: null,
65
+ };
20
66
  // Enough for a year of daily runs, pasted as one list.
21
67
  export const queueMax = 500;
22
68
  export const scheduleCreateSchema = z
@@ -39,6 +85,10 @@ export const scheduleCreateSchema = z
39
85
  topicKeyword: keywordName.nullable().default(null),
40
86
  // Keywords every run uses as they are, such as the word counts.
41
87
  values: values.default({}),
88
+ // What the channel covers, its style and what makes a topic worth watching. Only topic
89
+ // generation reads it.
90
+ brief: z.string().trim().max(briefMax).nullable().default(null),
91
+ topicGeneration: topicGenerationSchema.default(topicGenerationOff),
42
92
  })
43
93
  .strict()
44
94
  .readonly();
@@ -69,6 +119,11 @@ export const scheduleSummarySchema = z
69
119
  items: z.array(item).readonly(),
70
120
  topicKeyword: z.string().nullable().default(null),
71
121
  values: values.default({}),
122
+ brief: z.string().nullable().default(null),
123
+ topicGeneration: topicGenerationSchema.default(topicGenerationOff),
124
+ // The generation's own state: how many topics wait for approval, whether an LLM is being
125
+ // asked now, and the last failure's plain reason (cleared by the next success).
126
+ topics: topicState.default(topicStateIdle),
72
127
  status: z.enum(["active", "paused", "completed", "canceled"]),
73
128
  version: z.number().int().positive(),
74
129
  nextRunAt: z.string().datetime({ offset: true }).nullable(),
@@ -93,3 +148,85 @@ export const scheduleRunSchema = z
93
148
  })
94
149
  .strict()
95
150
  .readonly();
151
+ export const heldTopicSchema = z
152
+ .object({ id, title: z.string(), rank: z.number().int(), createdAt: z.string() })
153
+ .strict()
154
+ .readonly();
155
+ // Reordering is by position against the version the person saw, so a run taking the first
156
+ // topic meanwhile makes the move refuse instead of moving the wrong one.
157
+ export const topicMoveSchema = z
158
+ .object({
159
+ baseVersion: z.number().int().positive(),
160
+ from: z.number().int().nonnegative(),
161
+ to: z.number().int().nonnegative(),
162
+ })
163
+ .strict()
164
+ .readonly();
165
+ // The calendar: what will run, what is running or finished, and what waits in the batch queue.
166
+ // `topicSource` says where a run's topic comes from: the queue (`topic` names it), topics held
167
+ // for approval, a generation still to come, or none (the template as saved).
168
+ export const calendarRunSchema = z
169
+ .object({
170
+ at: z.string(),
171
+ scheduleId: id,
172
+ scheduleName: z.string(),
173
+ scheduleVersion: z.number().int().positive(),
174
+ paused: z.boolean(),
175
+ templateId: id,
176
+ templateVersion: z.number().int().positive(),
177
+ templateName: z.string().nullable(),
178
+ // The topic's position in the schedule's queue, for reordering; null without one.
179
+ index: z.number().int().nonnegative().nullable(),
180
+ topic: z.string().nullable(),
181
+ topicSource: z.enum(["queued", "held", "generated", "template"]),
182
+ // The project title this run will get, the way the scheduler builds it; null when it can't
183
+ // be known yet (a topic still to be approved or generated, or a deleted template). Older
184
+ // servers leave it out.
185
+ renderedTitle: z.string().nullable().default(null),
186
+ })
187
+ .strict()
188
+ .readonly();
189
+ export const calendarProjectSchema = z
190
+ .object({
191
+ id: z.string(),
192
+ title: z.string(),
193
+ state: z.enum(projectStates),
194
+ createdAt: z.string(),
195
+ finishedAt: z.string().nullable(),
196
+ scheduleId: z.string().nullable(),
197
+ })
198
+ .strict()
199
+ .readonly();
200
+ export const calendarQueuedSchema = z
201
+ .object({
202
+ projectId: z.string(),
203
+ title: z.string(),
204
+ batchId: z.string(),
205
+ position: z.number().int(),
206
+ state: z.enum(["queued", "active"]),
207
+ queuedAt: z.string(),
208
+ })
209
+ .strict()
210
+ .readonly();
211
+ export const calendarSchema = z
212
+ .object({
213
+ from: z.string(),
214
+ to: z.string(),
215
+ runs: z.array(calendarRunSchema).readonly(),
216
+ projects: z.array(calendarProjectSchema).readonly(),
217
+ queued: z.array(calendarQueuedSchema).readonly(),
218
+ })
219
+ .strict()
220
+ .readonly();
221
+ // A calendar covers at most this many days per request.
222
+ export const calendarMaxDays = 92;
223
+ export const topicTransferSchema = z
224
+ .object({
225
+ baseVersion: z.number().int().positive(),
226
+ index: z.number().int().nonnegative(),
227
+ targetId: id,
228
+ // Where it lands in the other schedule's queue; the end when left out.
229
+ position: z.number().int().nonnegative().optional(),
230
+ })
231
+ .strict()
232
+ .readonly();
@@ -3,7 +3,8 @@ import { transact } from "../../kernel/db/tx.js";
3
3
  import { requestHash } from "../play-drafts/repo.js";
4
4
  import { nextOccurrence } from "./calendar.js";
5
5
  import { insertSchedule, runsForSchedule, scheduleById, scheduleRows, setScheduleStatus, tombstoneScheduleRow, updateScheduleRow, } from "./repo.js";
6
- import { scheduleControlSchema, scheduleCreateSchema, scheduleDeleteSchema, scheduleUpdateSchema, } from "./schema.js";
6
+ import { scheduleControlSchema, scheduleCreateSchema, scheduleDeleteSchema, scheduleUpdateSchema, topicStateIdle, } from "./schema.js";
7
+ import { templateKeywords, topicRowProblems } from "./topic-list.js";
7
8
  export function listSchedules(deps) {
8
9
  return scheduleRows(deps.db);
9
10
  }
@@ -37,6 +38,9 @@ export function createSchedule(deps, input) {
37
38
  const checked = checkTemplate(deps, parsed.data.templateId, parsed.data.templateVersion);
38
39
  if (!checked.ok)
39
40
  return checked;
41
+ const topics = checkTopics(deps, parsed.data, []);
42
+ if (!topics.ok)
43
+ return topics;
40
44
  const now = deps.clock.now();
41
45
  const next = nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
42
46
  if (next === null)
@@ -76,13 +80,18 @@ export function updateSchedule(deps, input) {
76
80
  const checked = checkTemplate(deps, parsed.data.templateId, parsed.data.templateVersion);
77
81
  if (!checked.ok)
78
82
  return checked;
83
+ const topics = checkTopics(deps, parsed.data, previous.items);
84
+ if (!topics.ok)
85
+ return topics;
79
86
  const now = deps.clock.now();
80
87
  const next = previous.status === "active"
81
88
  ? nextOccurrence(parsed.data.cadence, parsed.data.timezone, now)
82
89
  : previous.nextRunAt === null
83
90
  ? null
84
91
  : nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
85
- const value = summary(parsed.data, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1);
92
+ const value = summary(parsed.data, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1,
93
+ // Saving clears a recorded generation failure, so the next tick tries again.
94
+ { ...previous.topics, failedAt: null, error: null });
86
95
  if (!updateScheduleRow(deps.db, value, parsed.data.mutationId, hash, parsed.data.baseVersion))
87
96
  return { ok: false, reason: "conflict" };
88
97
  return { ok: true, value };
@@ -154,14 +163,41 @@ function checkTemplate(deps, id, version) {
154
163
  const mediaSources = ["audio", "images", "thumbnail"];
155
164
  const { form } = template.document;
156
165
  // A supplied file is attached again on Play, which a scheduled run cannot do; the shorts'
157
- // background music is one too.
166
+ // background music is one too, and so is the ambient bed's own file.
158
167
  if (mediaSources.some((kind) => sources[kind] === "provide") ||
159
168
  (form.shorts?.enabled === true && form.provided.shortsMusic) ||
169
+ form.ambientBed?.source === "upload" ||
160
170
  (sources.images === "generate" && form.reference?.source === "provide"))
161
171
  return { ok: false, reason: "unsupported-media" };
162
172
  return { ok: true, value: true };
163
173
  }
164
- function summary(input, nextRunAt, now, status = "active", version = 1) {
174
+ // The queue's keywords must be the template's: a topic (or an every-run value) naming a keyword
175
+ // the template doesn't use would silently do nothing. Topics saved before, unchanged, are left
176
+ // alone, so a queue from before this check still saves.
177
+ function checkTopics(deps, input, kept) {
178
+ const template = deps.template(input.templateId, input.templateVersion);
179
+ if (!template)
180
+ return { ok: false, reason: "missing-template" };
181
+ const keywords = templateKeywords(template.document.form);
182
+ const named = keywords.length === 0 ? "none" : keywords.map((one) => `“${one}”`).join(", ");
183
+ const problems = [];
184
+ if (input.topicKeyword !== null && !keywords.includes(input.topicKeyword))
185
+ problems.push(`The topic keyword “${input.topicKeyword}” is not in this template (its keywords are ${named}). Pick another under Each topic fills.`);
186
+ for (const keyword of Object.keys(input.values))
187
+ if (!keywords.includes(keyword))
188
+ problems.push(`“${keyword}” (for every run) is not a keyword of this template (its keywords are ${named}). Remove it, or pick the template that uses it.`);
189
+ problems.push(...topicRowProblems(input.items, { keywords, topicKeyword: input.topicKeyword, kept }));
190
+ if (problems.length === 0)
191
+ return { ok: true, value: true };
192
+ const shown = problems.slice(0, 5);
193
+ const more = problems.length - shown.length;
194
+ return {
195
+ ok: false,
196
+ reason: "invalid-topics",
197
+ message: `The schedule wasn't saved. ${shown.join(" ")}${more > 0 ? ` And ${String(more)} more like these.` : ""}`,
198
+ };
199
+ }
200
+ function summary(input, nextRunAt, now, status = "active", version = 1, topics = topicStateIdle) {
165
201
  return {
166
202
  id: input.id,
167
203
  name: input.name,
@@ -175,6 +211,9 @@ function summary(input, nextRunAt, now, status = "active", version = 1) {
175
211
  items: input.items,
176
212
  topicKeyword: input.topicKeyword,
177
213
  values: input.values,
214
+ brief: input.brief === null || input.brief === "" ? null : input.brief,
215
+ topicGeneration: input.topicGeneration,
216
+ topics,
178
217
  status,
179
218
  version,
180
219
  nextRunAt,
@@ -0,0 +1,90 @@
1
+ // Near-duplicate checks for generated topics. Pure, so the web bundle can import it too.
2
+ //
3
+ // Two topics are the same when, after normalising (accents dropped, lower case, punctuation
4
+ // to spaces, "&" read as "and"):
5
+ // - the texts are equal, or
6
+ // - their word sets overlap by at least `jaccardMin` (shared words / all words), or
7
+ // - one's words all appear in the other's and cover at least `containmentMin` of it
8
+ // ("Tiamat" and "Tiamat's Lair" are one video; "Red Dragons" and "Blue Dragons" are two).
9
+ // Words are compared without filler words ("the", "of", …) and without a plural "s".
10
+ // A project title is compared more loosely: every word of the topic appearing in it is enough,
11
+ // because the title wraps the topic in the template's own words ("D&D Lore: Tiamat").
12
+ export const jaccardMin = 0.6;
13
+ export const containmentMin = 0.5;
14
+ const filler = new Set([
15
+ "a",
16
+ "an",
17
+ "the",
18
+ "of",
19
+ "and",
20
+ "or",
21
+ "in",
22
+ "on",
23
+ "to",
24
+ "for",
25
+ "with",
26
+ "vs",
27
+ "s",
28
+ ]);
29
+ export function normaliseTopic(text) {
30
+ return text
31
+ .normalize("NFKD")
32
+ .replace(/\p{M}/gu, "")
33
+ .toLowerCase()
34
+ .replace(/&/g, " and ")
35
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
36
+ .trim();
37
+ }
38
+ export function topicWords(text) {
39
+ const words = normaliseTopic(text)
40
+ .split(" ")
41
+ .filter((word) => word !== "" && !filler.has(word))
42
+ .map((word) => word.length > 3 && word.endsWith("s") && !word.endsWith("ss") ? word.slice(0, -1) : word);
43
+ return new Set(words);
44
+ }
45
+ function shared(a, b) {
46
+ let count = 0;
47
+ for (const word of a)
48
+ if (b.has(word))
49
+ count += 1;
50
+ return count;
51
+ }
52
+ export function similarTopics(a, b) {
53
+ const left = normaliseTopic(a);
54
+ const right = normaliseTopic(b);
55
+ if (left === right)
56
+ return left !== "";
57
+ const wa = topicWords(a);
58
+ const wb = topicWords(b);
59
+ if (wa.size === 0 || wb.size === 0)
60
+ return false;
61
+ const common = shared(wa, wb);
62
+ if (common / (wa.size + wb.size - common) >= jaccardMin)
63
+ return true;
64
+ const small = Math.min(wa.size, wb.size);
65
+ const large = Math.max(wa.size, wb.size);
66
+ return common === small && small / large >= containmentMin;
67
+ }
68
+ // A topic already made as a project: all of its words appear in the project's title.
69
+ export function topicInTitle(topic, title) {
70
+ if (similarTopics(topic, title))
71
+ return true;
72
+ const words = topicWords(topic);
73
+ if (words.size === 0)
74
+ return false;
75
+ return shared(words, topicWords(title)) === words.size;
76
+ }
77
+ // Keeps the candidates that are new, in their order, dropping repeats among themselves too.
78
+ export function newTopics(candidates, known) {
79
+ const kept = [];
80
+ for (const candidate of candidates) {
81
+ if (known.topics.some((topic) => similarTopics(candidate, topic)))
82
+ continue;
83
+ if (known.projects.some((title) => topicInTitle(candidate, title)))
84
+ continue;
85
+ if (kept.some((topic) => similarTopics(candidate, topic)))
86
+ continue;
87
+ kept.push(candidate);
88
+ }
89
+ return kept;
90
+ }