@gentbajko/slopify 2.5.0 → 3.0.1

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 (542) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +24 -41
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +31 -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 +85 -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/gemini.js +206 -0
  44. package/dist/adapters/tts/inworld-async.js +9 -5
  45. package/dist/adapters/tts/inworld.js +17 -3
  46. package/dist/adapters/tts/openai.js +3 -0
  47. package/dist/adapters/tts/pcm-mp3.js +62 -0
  48. package/dist/adapters/tts/system.js +147 -0
  49. package/dist/adapters/tts/voice-languages.js +80 -0
  50. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  55. package/dist/assets/fonts/noto/OFL.txt +93 -0
  56. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  57. package/dist/assets/models.yaml +38 -0
  58. package/dist/assets/sample/sample-audiobook.tar +0 -0
  59. package/dist/assets/sample/sample-podcast.tar +0 -0
  60. package/dist/assets/sample/sample-project.tar +0 -0
  61. package/dist/assets/style-preview/narration.mp3 +0 -0
  62. package/dist/assets/style-preview/tall-1.jpg +0 -0
  63. package/dist/assets/style-preview/tall-2.jpg +0 -0
  64. package/dist/assets/style-preview/tall-3.jpg +0 -0
  65. package/dist/assets/style-preview/wide-1.jpg +0 -0
  66. package/dist/assets/style-preview/wide-2.jpg +0 -0
  67. package/dist/assets/style-preview/wide-3.jpg +0 -0
  68. package/dist/catalog/merge.js +132 -0
  69. package/dist/catalog/registry.js +17 -14
  70. package/dist/catalog/schema.js +2 -0
  71. package/dist/catalog/store.js +147 -8
  72. package/dist/catalog/validate.js +2 -2
  73. package/dist/compose.yaml +71 -0
  74. package/dist/edge/autostart/docker-record.js +32 -0
  75. package/dist/edge/autostart/docker.js +118 -0
  76. package/dist/edge/autostart/index.js +78 -0
  77. package/dist/edge/autostart/launcher.js +176 -0
  78. package/dist/edge/autostart/model.js +3 -0
  79. package/dist/edge/autostart/native.js +210 -0
  80. package/dist/edge/autostart/prompt.js +72 -0
  81. package/dist/edge/autostart/service.js +90 -0
  82. package/dist/edge/cli-args.js +71 -0
  83. package/dist/edge/cli.js +64 -63
  84. package/dist/edge/docker-install/activation.js +56 -0
  85. package/dist/edge/docker-install/apply.js +458 -0
  86. package/dist/edge/docker-install/engine.js +362 -0
  87. package/dist/edge/docker-install/run.js +160 -0
  88. package/dist/edge/docker-install/state.js +146 -0
  89. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  90. package/dist/edge/docker.js +14 -22
  91. package/dist/edge/events/hub.js +3 -1
  92. package/dist/edge/host-cli.js +1 -1
  93. package/dist/edge/http/actions.js +38 -11
  94. package/dist/edge/http/app.js +60 -2
  95. package/dist/edge/http/auditions.js +70 -0
  96. package/dist/edge/http/autostart.js +37 -0
  97. package/dist/edge/http/byte-range.js +42 -0
  98. package/dist/edge/http/channel-memory.js +92 -0
  99. package/dist/edge/http/channels.js +128 -0
  100. package/dist/edge/http/diagnostics.js +1 -0
  101. package/dist/edge/http/draft-files.js +2 -4
  102. package/dist/edge/http/entries.js +22 -3
  103. package/dist/edge/http/files.js +11 -7
  104. package/dist/edge/http/folder-location.js +1 -1
  105. package/dist/edge/http/home.js +39 -0
  106. package/dist/edge/http/host-cli.js +29 -5
  107. package/dist/edge/http/narration-peaks.js +76 -0
  108. package/dist/edge/http/onboarding.js +212 -0
  109. package/dist/edge/http/open-folder.js +1 -1
  110. package/dist/edge/http/patch-notes.js +45 -0
  111. package/dist/edge/http/planning.js +4 -2
  112. package/dist/edge/http/project-create.js +87 -0
  113. package/dist/edge/http/project-templates.js +19 -2
  114. package/dist/edge/http/projects.js +37 -100
  115. package/dist/edge/http/prompts.js +40 -3
  116. package/dist/edge/http/pronunciations.js +23 -4
  117. package/dist/edge/http/providers.js +119 -3
  118. package/dist/edge/http/reviews.js +131 -0
  119. package/dist/edge/http/revision-files.js +3 -5
  120. package/dist/edge/http/revisions.js +1 -1
  121. package/dist/edge/http/run-cost.js +25 -0
  122. package/dist/edge/http/schedules.js +115 -6
  123. package/dist/edge/http/settings.js +84 -6
  124. package/dist/edge/http/storage-files.js +50 -0
  125. package/dist/edge/http/storage.js +43 -2
  126. package/dist/edge/http/studio.js +287 -0
  127. package/dist/edge/http/style-preview.js +61 -0
  128. package/dist/edge/http/subtitles.js +1 -1
  129. package/dist/edge/http/trash.js +69 -0
  130. package/dist/edge/http/tutorials.js +59 -0
  131. package/dist/edge/http/update.js +24 -3
  132. package/dist/edge/http/whats-new.js +15 -0
  133. package/dist/edge/http/youtube-edits.js +92 -0
  134. package/dist/edge/native-update.js +62 -0
  135. package/dist/extension/slopify-studio-chrome.zip +0 -0
  136. package/dist/extension/slopify-studio-firefox.zip +0 -0
  137. package/dist/host-cli/gemini-login.js +83 -0
  138. package/dist/host-cli/open-folder.js +10 -10
  139. package/dist/host-cli/runtime.js +9 -1
  140. package/dist/host-cli/status.js +4 -2
  141. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  142. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  143. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  144. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  145. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  146. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  147. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  148. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  149. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  150. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  151. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  152. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  153. package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
  154. package/dist/kernel/paths.js +27 -6
  155. package/dist/kernel/pipeline.js +4 -1
  156. package/dist/kernel/ports/host-cli.js +122 -14
  157. package/dist/kernel/ports/key-probe.js +1 -0
  158. package/dist/kernel/ports/languages.js +157 -0
  159. package/dist/kernel/ports/model.js +10 -5
  160. package/dist/kernel/ports/narration-aliases.js +60 -0
  161. package/dist/kernel/ports/plan-limits.js +36 -0
  162. package/dist/kernel/ports/system-speech.js +203 -0
  163. package/dist/kernel/runner/attempt.js +17 -8
  164. package/dist/kernel/runner/audition.js +36 -0
  165. package/dist/kernel/runner/graph.js +40 -5
  166. package/dist/kernel/runner/index.js +39 -9
  167. package/dist/kernel/runner/meter.js +1 -0
  168. package/dist/kernel/runner/providers.js +142 -7
  169. package/dist/kernel/runner/retry-policy.js +43 -0
  170. package/dist/kernel/runner/standalone.js +134 -0
  171. package/dist/kernel/runner/work-authority.js +28 -2
  172. package/dist/main.js +277 -16
  173. package/dist/model-catalog.js +2 -0
  174. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  175. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  176. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  177. package/dist/patch-notes/3.0.0.md +70 -0
  178. package/dist/patch-notes/3.0.1.md +105 -0
  179. package/dist/patch-notes/index.json +32 -0
  180. package/dist/slices/admission/model.js +27 -0
  181. package/dist/slices/admission/repo.js +24 -4
  182. package/dist/slices/admission/rules.js +80 -2
  183. package/dist/slices/admission/schema.js +58 -1
  184. package/dist/slices/admission/short-mode.js +47 -0
  185. package/dist/slices/admission/start.js +42 -6
  186. package/dist/slices/article/continuation.js +2 -2
  187. package/dist/slices/article/provided-entries.js +2 -2
  188. package/dist/slices/article/run.js +6 -2
  189. package/dist/slices/article/segments.js +3 -3
  190. package/dist/slices/backups/folder.js +14 -6
  191. package/dist/slices/batch/index.js +7 -2
  192. package/dist/slices/cancel/index.js +1 -1
  193. package/dist/slices/channels/cast-images.js +110 -0
  194. package/dist/slices/channels/cast-match.js +32 -0
  195. package/dist/slices/channels/images.js +19 -0
  196. package/dist/slices/channels/model.js +5 -0
  197. package/dist/slices/channels/rebrand.js +100 -0
  198. package/dist/slices/channels/repo.js +221 -0
  199. package/dist/slices/channels/runs.js +129 -0
  200. package/dist/slices/channels/schema.js +152 -0
  201. package/dist/slices/channels/service.js +180 -0
  202. package/dist/slices/channels/studio-csv.js +110 -0
  203. package/dist/slices/channels/videos.js +120 -0
  204. package/dist/slices/checkpoints/recovery.js +10 -0
  205. package/dist/slices/control/index.js +5 -2
  206. package/dist/slices/control/providers.js +4 -1
  207. package/dist/slices/document/fonts.js +1 -1
  208. package/dist/slices/document/render.js +1 -1
  209. package/dist/slices/episodes/related.js +90 -0
  210. package/dist/slices/episodes/repo.js +60 -0
  211. package/dist/slices/episodes/service.js +58 -0
  212. package/dist/slices/episodes/summarize.js +170 -0
  213. package/dist/slices/estimate/index.js +109 -28
  214. package/dist/slices/estimate/requests.js +68 -2
  215. package/dist/slices/eta/history.js +56 -0
  216. package/dist/slices/eta/model.js +63 -0
  217. package/dist/slices/eta/view.js +25 -0
  218. package/dist/slices/fixes/rules.js +73 -0
  219. package/dist/slices/fonts/coverage.js +82 -0
  220. package/dist/slices/fonts/sfnt.js +74 -0
  221. package/dist/slices/images/run.js +8 -5
  222. package/dist/slices/images/scale.js +84 -0
  223. package/dist/slices/library/diff.js +122 -0
  224. package/dist/slices/library/history.js +100 -0
  225. package/dist/slices/library/model.js +3 -0
  226. package/dist/slices/library/photorealistic.js +49 -0
  227. package/dist/slices/library/repo.js +19 -14
  228. package/dist/slices/library/save.js +62 -12
  229. package/dist/slices/library/slots.js +7 -1
  230. package/dist/slices/library/used-by.js +153 -0
  231. package/dist/slices/loudness/level-pieces.js +41 -0
  232. package/dist/slices/loudness/loudnorm.js +164 -0
  233. package/dist/slices/loudness/model.js +153 -0
  234. package/dist/slices/model-upkeep/model.js +10 -0
  235. package/dist/slices/model-upkeep/switch.js +217 -0
  236. package/dist/slices/model-upkeep/usage.js +185 -0
  237. package/dist/slices/narration/aliases-library.js +31 -0
  238. package/dist/slices/narration/aliases-schema.js +53 -0
  239. package/dist/slices/narration/aliases.js +49 -0
  240. package/dist/slices/narration/blocks.js +352 -0
  241. package/dist/slices/narration/chunk.js +19 -7
  242. package/dist/slices/narration/concat.js +2 -2
  243. package/dist/slices/narration/describe.js +45 -0
  244. package/dist/slices/narration/pauses-model.js +63 -0
  245. package/dist/slices/narration/pauses.js +269 -0
  246. package/dist/slices/narration/peaks-model.js +1 -0
  247. package/dist/slices/narration/peaks.js +94 -0
  248. package/dist/slices/narration/preparation.js +21 -3
  249. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  250. package/dist/slices/narration/pronunciation.js +72 -25
  251. package/dist/slices/narration/run.js +6 -6
  252. package/dist/slices/narration/shared-glossary.js +1 -1
  253. package/dist/slices/narration/steering.js +15 -4
  254. package/dist/slices/notifications/notifier.js +47 -3
  255. package/dist/slices/notifications/rules.js +48 -3
  256. package/dist/slices/onboarding/copy.js +143 -0
  257. package/dist/slices/onboarding/first-run.js +72 -0
  258. package/dist/slices/onboarding/full-video.js +81 -0
  259. package/dist/slices/onboarding/install.js +119 -0
  260. package/dist/slices/onboarding/model.js +85 -0
  261. package/dist/slices/onboarding/packs.js +491 -0
  262. package/dist/slices/onboarding/quick-short.js +144 -0
  263. package/dist/slices/onboarding/sample.js +132 -0
  264. package/dist/slices/onboarding/state.js +68 -0
  265. package/dist/slices/onboarding/template.js +73 -0
  266. package/dist/slices/patch-notes/library.js +73 -0
  267. package/dist/slices/patch-notes/seen.js +77 -0
  268. package/dist/slices/play-drafts/convert.js +127 -8
  269. package/dist/slices/play-drafts/readiness.js +4 -1
  270. package/dist/slices/play-drafts/repo.js +2 -0
  271. package/dist/slices/play-drafts/review-inputs.js +20 -3
  272. package/dist/slices/play-drafts/schema.js +89 -0
  273. package/dist/slices/play-drafts/service.js +4 -1
  274. package/dist/slices/play-drafts/start.js +43 -18
  275. package/dist/slices/project-templates/from-project.js +59 -6
  276. package/dist/slices/project-templates/next-chapter.js +94 -0
  277. package/dist/slices/project-templates/one-off.js +26 -0
  278. package/dist/slices/project-templates/repo.js +2 -1
  279. package/dist/slices/project-templates/schema.js +7 -1
  280. package/dist/slices/project-templates/service.js +15 -4
  281. package/dist/slices/project-templates/setup.js +18 -2
  282. package/dist/slices/rebuild/admission-repo.js +1 -1
  283. package/dist/slices/rebuild/model.js +6 -0
  284. package/dist/slices/rebuild/preview-details.js +46 -3
  285. package/dist/slices/rebuild/preview-plan.js +13 -6
  286. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  287. package/dist/slices/rebuild/recipe-audio.js +66 -12
  288. package/dist/slices/rebuild/recipe-build.js +8 -4
  289. package/dist/slices/rebuild/recipe-cast.js +18 -0
  290. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  291. package/dist/slices/rebuild/recipe-describe.js +93 -0
  292. package/dist/slices/rebuild/recipe-edit.js +13 -1
  293. package/dist/slices/rebuild/recipe-exports.js +67 -8
  294. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  295. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  296. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  297. package/dist/slices/rebuild/recipe-model.js +25 -6
  298. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  299. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  300. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  301. package/dist/slices/rebuild/recipe-reference.js +15 -1
  302. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  303. package/dist/slices/rebuild/recipe-save.js +4 -1
  304. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  305. package/dist/slices/rebuild/recipe-text.js +114 -19
  306. package/dist/slices/rebuild/recipe-validation.js +14 -2
  307. package/dist/slices/rebuild/recipe-visual.js +96 -33
  308. package/dist/slices/rebuild/recipe-voices.js +286 -0
  309. package/dist/slices/rebuild/recipe-work.js +77 -6
  310. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  311. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  312. package/dist/slices/rebuild/recovery-model.js +3 -0
  313. package/dist/slices/rebuild/recovery-selection.js +28 -2
  314. package/dist/slices/rebuild/recovery.js +35 -17
  315. package/dist/slices/rebuild/repo.js +3 -3
  316. package/dist/slices/rebuild/retry.js +18 -0
  317. package/dist/slices/rebuild/review-redo.js +125 -0
  318. package/dist/slices/rebuild/runtime-actions.js +5 -0
  319. package/dist/slices/rebuild/runtime-admission.js +4 -1
  320. package/dist/slices/rebuild/runtime-animate.js +4 -4
  321. package/dist/slices/rebuild/runtime-article.js +5 -5
  322. package/dist/slices/rebuild/runtime-document.js +4 -4
  323. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  324. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  325. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  326. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  327. package/dist/slices/rebuild/runtime-export.js +84 -19
  328. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  329. package/dist/slices/rebuild/runtime-image.js +21 -2
  330. package/dist/slices/rebuild/runtime-local.js +148 -24
  331. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  332. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  333. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  334. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  335. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  336. package/dist/slices/rebuild/runtime-provider.js +126 -18
  337. package/dist/slices/rebuild/runtime-publication.js +21 -5
  338. package/dist/slices/rebuild/runtime-review.js +267 -0
  339. package/dist/slices/rebuild/runtime-run.js +17 -11
  340. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  341. package/dist/slices/rebuild/runtime-store.js +62 -11
  342. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  343. package/dist/slices/rebuild/runtime-voices.js +103 -0
  344. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  345. package/dist/slices/rebuild/service-readiness.js +3 -3
  346. package/dist/slices/rebuild/soften.js +49 -0
  347. package/dist/slices/rebuild/transition-repo.js +9 -2
  348. package/dist/slices/research/run.js +4 -4
  349. package/dist/slices/research/synthesis.js +2 -2
  350. package/dist/slices/reviews/model.js +105 -0
  351. package/dist/slices/reviews/outcome.js +22 -0
  352. package/dist/slices/reviews/repo.js +118 -0
  353. package/dist/slices/reviews/rules.js +63 -0
  354. package/dist/slices/reviews/verdict.js +71 -0
  355. package/dist/slices/reviews/view.js +30 -0
  356. package/dist/slices/revisions/adopt-content.js +5 -2
  357. package/dist/slices/revisions/adopt.js +3 -1
  358. package/dist/slices/revisions/downloads.js +7 -1
  359. package/dist/slices/revisions/image-plan.js +183 -0
  360. package/dist/slices/revisions/mutation-assets.js +10 -0
  361. package/dist/slices/revisions/mutation-cues.js +71 -0
  362. package/dist/slices/revisions/mutation-request.js +2 -1
  363. package/dist/slices/revisions/mutations.js +97 -3
  364. package/dist/slices/revisions/schema.js +11 -1
  365. package/dist/slices/run-cost/limits.js +128 -0
  366. package/dist/slices/run-cost/meter.js +47 -0
  367. package/dist/slices/run-cost/panel.js +256 -0
  368. package/dist/slices/run-cost/pricing.js +95 -0
  369. package/dist/slices/run-cost/week.js +108 -0
  370. package/dist/slices/schedules/agenda.js +222 -0
  371. package/dist/slices/schedules/repo.js +58 -18
  372. package/dist/slices/schedules/scheduler.js +68 -17
  373. package/dist/slices/schedules/schema.js +179 -1
  374. package/dist/slices/schedules/service.js +43 -4
  375. package/dist/slices/schedules/similar.js +128 -0
  376. package/dist/slices/schedules/topic-list.js +163 -0
  377. package/dist/slices/schedules/topics.js +523 -0
  378. package/dist/slices/settings/first-run.js +106 -0
  379. package/dist/slices/settings/health.js +247 -0
  380. package/dist/slices/settings/key-guides.js +138 -0
  381. package/dist/slices/settings/key-test.js +93 -0
  382. package/dist/slices/settings/keys.js +10 -5
  383. package/dist/slices/settings/model-reach.js +52 -0
  384. package/dist/slices/settings/model.js +55 -1
  385. package/dist/slices/settings/models.js +5 -1
  386. package/dist/slices/settings/playback.js +15 -0
  387. package/dist/slices/settings/readiness.js +14 -2
  388. package/dist/slices/settings/repo.js +17 -3
  389. package/dist/slices/settings/tutorial-schema.js +5 -0
  390. package/dist/slices/settings/voices.js +64 -4
  391. package/dist/slices/settings/whats-new.js +64 -0
  392. package/dist/slices/shorts/captions.js +9 -0
  393. package/dist/slices/shorts/pick.js +3 -3
  394. package/dist/slices/shorts/prompts.js +1 -1
  395. package/dist/slices/shorts/render.js +63 -13
  396. package/dist/slices/storage/asset-name.js +3 -0
  397. package/dist/slices/storage/backup-export.js +48 -9
  398. package/dist/slices/storage/backup-format.js +38 -1
  399. package/dist/slices/storage/backup-import.js +191 -19
  400. package/dist/slices/storage/delete-project.js +7 -4
  401. package/dist/slices/storage/documents.js +87 -0
  402. package/dist/slices/storage/downloads.js +33 -1
  403. package/dist/slices/storage/files-location.js +531 -0
  404. package/dist/slices/storage/layout.js +14 -4
  405. package/dist/slices/storage/model.js +11 -0
  406. package/dist/slices/storage/portable.js +94 -12
  407. package/dist/slices/storage/reconcile.js +14 -1
  408. package/dist/slices/storage/repo.js +4 -1
  409. package/dist/slices/storage/schema.js +11 -0
  410. package/dist/slices/storage/trim.js +146 -0
  411. package/dist/slices/studio/disclosure.js +74 -0
  412. package/dist/slices/studio/model.js +30 -0
  413. package/dist/slices/studio/pack.js +211 -0
  414. package/dist/slices/studio/queue.js +69 -0
  415. package/dist/slices/studio/settings.js +170 -0
  416. package/dist/slices/style-preview/images.js +61 -0
  417. package/dist/slices/style-preview/narration.js +33 -0
  418. package/dist/slices/style-preview/render.js +148 -0
  419. package/dist/slices/style-preview/schema.js +59 -0
  420. package/dist/slices/style-preview/service.js +92 -0
  421. package/dist/slices/style-preview/settings.js +139 -0
  422. package/dist/slices/subtitles/captions.js +42 -9
  423. package/dist/slices/subtitles/model.js +22 -0
  424. package/dist/slices/subtitles/prepare.js +27 -16
  425. package/dist/slices/subtitles/transcript.js +1 -1
  426. package/dist/slices/thumbnail/run.js +7 -7
  427. package/dist/slices/trash/model.js +25 -0
  428. package/dist/slices/trash/service.js +273 -0
  429. package/dist/slices/tutorials/anchors.js +57 -0
  430. package/dist/slices/tutorials/library.js +185 -0
  431. package/dist/slices/uploads/repo.js +20 -0
  432. package/dist/slices/video/ambient-bed-schema.js +15 -0
  433. package/dist/slices/video/ambient-bed.js +95 -0
  434. package/dist/slices/video/ambient-mix.js +72 -0
  435. package/dist/slices/video/audio-inputs.js +2 -2
  436. package/dist/slices/video/cards.js +5 -2
  437. package/dist/slices/video/edit-list.js +23 -2
  438. package/dist/slices/video/edit-settings.js +13 -3
  439. package/dist/slices/video/ffmpeg.js +63 -9
  440. package/dist/slices/video/figure-card.js +492 -0
  441. package/dist/slices/video/figure-spans.js +51 -0
  442. package/dist/slices/video/plan.js +125 -13
  443. package/dist/slices/video/run.js +14 -12
  444. package/dist/slices/video/slideshow.js +39 -4
  445. package/dist/slices/voices/attribution.js +69 -0
  446. package/dist/slices/voices/audio-files.js +87 -0
  447. package/dist/slices/voices/audition.js +25 -0
  448. package/dist/slices/voices/cast.js +74 -0
  449. package/dist/slices/voices/grouping.js +59 -0
  450. package/dist/slices/voices/join.js +35 -0
  451. package/dist/slices/voices/languages.js +25 -0
  452. package/dist/slices/voices/model.js +218 -0
  453. package/dist/slices/voices/palette.js +26 -0
  454. package/dist/slices/voices/panel.js +91 -0
  455. package/dist/slices/voices/portraits.js +55 -0
  456. package/dist/slices/voices/script.js +136 -0
  457. package/dist/slices/voices/timing.js +58 -0
  458. package/dist/slices/youtube/answer.js +1 -1
  459. package/dist/slices/youtube/chapters.js +126 -0
  460. package/dist/slices/youtube/edits-repo.js +84 -0
  461. package/dist/slices/youtube/edits.js +89 -0
  462. package/dist/slices/youtube/placeholders.js +98 -0
  463. package/dist/slices/youtube/transcript.js +11 -4
  464. package/dist/tutorials/AI-CLIs.md +146 -0
  465. package/dist/tutorials/Backups.md +121 -0
  466. package/dist/tutorials/Calendar.md +103 -0
  467. package/dist/tutorials/Cast-Library.md +112 -0
  468. package/dist/tutorials/Channels.md +148 -0
  469. package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
  470. package/dist/tutorials/Docker.md +171 -0
  471. package/dist/tutorials/Document-Themes.md +207 -0
  472. package/dist/tutorials/Editing-a-Project.md +204 -0
  473. package/dist/tutorials/FAQ.md +134 -0
  474. package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
  475. package/dist/tutorials/Home-and-Projects.md +174 -0
  476. package/dist/tutorials/Home.md +119 -0
  477. package/dist/tutorials/Install.md +154 -0
  478. package/dist/tutorials/Intros-and-Outros.md +86 -0
  479. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
  480. package/dist/tutorials/Library-Overview.md +75 -0
  481. package/dist/tutorials/Models.md +133 -0
  482. package/dist/tutorials/Multiple-Voices.md +161 -0
  483. package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
  484. package/dist/tutorials/Notifications.md +86 -0
  485. package/dist/tutorials/Other-Languages.md +110 -0
  486. package/dist/tutorials/PDF-Documents.md +100 -0
  487. package/dist/tutorials/Play-Images.md +135 -0
  488. package/dist/tutorials/Play-Narration.md +167 -0
  489. package/dist/tutorials/Play-Outputs.md +109 -0
  490. package/dist/tutorials/Play-Overview.md +216 -0
  491. package/dist/tutorials/Play-Title-and-Article.md +135 -0
  492. package/dist/tutorials/Play-Video-and-Style.md +143 -0
  493. package/dist/tutorials/Project-Page.md +228 -0
  494. package/dist/tutorials/Prompts.md +136 -0
  495. package/dist/tutorials/Providers-and-Keys.md +290 -0
  496. package/dist/tutorials/Publishing-to-YouTube.md +135 -0
  497. package/dist/tutorials/Recovery-and-Retries.md +141 -0
  498. package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
  499. package/dist/tutorials/Schedules.md +154 -0
  500. package/dist/tutorials/Settings-Reference.md +258 -0
  501. package/dist/tutorials/Shorts.md +116 -0
  502. package/dist/tutorials/Start-at-Login.md +126 -0
  503. package/dist/tutorials/Studio-Extension.md +146 -0
  504. package/dist/tutorials/Templates.md +146 -0
  505. package/dist/tutorials/Trash.md +87 -0
  506. package/dist/tutorials/Troubleshooting.md +149 -0
  507. package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
  508. package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
  509. package/dist/tutorials/Video-Editing.md +135 -0
  510. package/dist/tutorials/Where-Your-Files-Live.md +149 -0
  511. package/dist/tutorials/YouTube-Description.md +132 -0
  512. package/dist/tutorials/Your-First-Short.md +125 -0
  513. package/dist/tutorials/_Footer.md +1 -0
  514. package/dist/tutorials/_Sidebar.md +69 -0
  515. package/dist/updater/service.js +66 -21
  516. package/dist/web/assets/index-DBLSfqA-.css +1 -0
  517. package/dist/web/assets/index-DFdIhmm0.js +307 -0
  518. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  519. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  520. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  521. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  522. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  523. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  524. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  525. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  526. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  527. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
  528. package/dist/web/index.html +2 -2
  529. package/package.json +5 -5
  530. package/dist/edge/docker-launch.js +0 -81
  531. package/dist/edge/docker-projects/activation.js +0 -35
  532. package/dist/edge/docker-projects/claims.js +0 -85
  533. package/dist/edge/docker-projects/committed.js +0 -40
  534. package/dist/edge/docker-projects/engine.js +0 -461
  535. package/dist/edge/docker-projects/install.js +0 -254
  536. package/dist/edge/docker-projects/prune.js +0 -66
  537. package/dist/edge/docker-projects/recover.js +0 -100
  538. package/dist/edge/docker-projects/state.js +0 -225
  539. package/dist/web/assets/index-CktpxlJd.js +0 -171
  540. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  541. package/scripts/docker-run.sh +0 -3
  542. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod";
2
+ import { projectStates, stageKinds } 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();
6
+ // What a project on the calendar waits on the person for: a failed or paused run, or a review
7
+ // (a checkpoint holding the run, or an automatic review's failed item).
8
+ export const calendarNeeds = ["failed", "paused", "review"];
4
9
  const keywordName = z
5
10
  .string()
6
11
  .min(1)
@@ -9,14 +14,58 @@ const keywordName = z
9
14
  const values = z.record(keywordName, z.string().max(10000)).readonly();
10
15
  // A queued topic. Each run takes the first one and removes it once its project starts.
11
16
  // `values` is only filled on schedules saved before topics had one keyword.
17
+ export const topicTitleSchema = z.string().trim().min(1).max(200);
12
18
  const item = z
13
19
  .object({
14
- title: z.string().trim().min(1).max(200),
20
+ title: topicTitleSchema,
15
21
  values,
16
22
  })
17
23
  .strict()
18
24
  .readonly();
19
25
  const policy = z.enum(["skip", "run-once"]);
26
+ export const topicModes = ["off", "queue", "hold"];
27
+ export const topicMinDefault = 10;
28
+ export const briefMax = 4000;
29
+ // Whether a schedule asks an LLM for topics when its queue runs low, and whether they join the
30
+ // queue at once or wait for approval. `llm` is null for the template's own LLM.
31
+ export const topicGenerationSchema = z
32
+ .object({
33
+ mode: z.enum(topicModes),
34
+ keepAtLeast: z.number().int().min(1).max(100),
35
+ llm: z
36
+ .object({
37
+ provider: z.string().min(1).max(100),
38
+ model: z.string().min(1).max(200),
39
+ thinking: z.enum(thinkingModes).optional(),
40
+ })
41
+ .strict()
42
+ .readonly()
43
+ .nullable(),
44
+ })
45
+ .strict()
46
+ .readonly();
47
+ export const topicGenerationOff = {
48
+ mode: "off",
49
+ keepAtLeast: topicMinDefault,
50
+ llm: null,
51
+ };
52
+ const topicState = z
53
+ .object({
54
+ held: z.number().int().nonnegative(),
55
+ generatingSince: z.string().nullable(),
56
+ generatedAt: z.string().nullable(),
57
+ failedAt: z.string().nullable(),
58
+ error: z.string().nullable(),
59
+ })
60
+ .strict()
61
+ .readonly();
62
+ export const topicStateIdle = {
63
+ held: 0,
64
+ generatingSince: null,
65
+ generatedAt: null,
66
+ failedAt: null,
67
+ error: null,
68
+ };
20
69
  // Enough for a year of daily runs, pasted as one list.
21
70
  export const queueMax = 500;
22
71
  export const scheduleCreateSchema = z
@@ -39,6 +88,10 @@ export const scheduleCreateSchema = z
39
88
  topicKeyword: keywordName.nullable().default(null),
40
89
  // Keywords every run uses as they are, such as the word counts.
41
90
  values: values.default({}),
91
+ // What the channel covers, its style and what makes a topic worth watching. Only topic
92
+ // generation reads it.
93
+ brief: z.string().trim().max(briefMax).nullable().default(null),
94
+ topicGeneration: topicGenerationSchema.default(topicGenerationOff),
42
95
  })
43
96
  .strict()
44
97
  .readonly();
@@ -69,6 +122,11 @@ export const scheduleSummarySchema = z
69
122
  items: z.array(item).readonly(),
70
123
  topicKeyword: z.string().nullable().default(null),
71
124
  values: values.default({}),
125
+ brief: z.string().nullable().default(null),
126
+ topicGeneration: topicGenerationSchema.default(topicGenerationOff),
127
+ // The generation's own state: how many topics wait for approval, whether an LLM is being
128
+ // asked now, and the last failure's plain reason (cleared by the next success).
129
+ topics: topicState.default(topicStateIdle),
72
130
  status: z.enum(["active", "paused", "completed", "canceled"]),
73
131
  version: z.number().int().positive(),
74
132
  nextRunAt: z.string().datetime({ offset: true }).nullable(),
@@ -93,3 +151,123 @@ export const scheduleRunSchema = z
93
151
  })
94
152
  .strict()
95
153
  .readonly();
154
+ // `values` are the keywords the person set on a held topic before approving it; they go with it
155
+ // into the queue. Absent from a reply of a Slopify before they existed: none.
156
+ export const heldTopicSchema = z
157
+ .object({
158
+ id,
159
+ title: z.string(),
160
+ values: z.record(z.string(), z.string()).readonly().default({}),
161
+ rank: z.number().int(),
162
+ createdAt: z.string(),
163
+ })
164
+ .strict()
165
+ .readonly();
166
+ // Edit on a held topic: its title, and its keywords when given (replacing the ones it had).
167
+ export const heldTopicEditSchema = z
168
+ .object({ title: z.string(), values: z.record(keywordName, z.string()).optional() })
169
+ .strict();
170
+ // Reordering is by position against the version the person saw, so a run taking the first
171
+ // topic meanwhile makes the move refuse instead of moving the wrong one.
172
+ export const topicMoveSchema = z
173
+ .object({
174
+ baseVersion: z.number().int().positive(),
175
+ from: z.number().int().nonnegative(),
176
+ to: z.number().int().nonnegative(),
177
+ })
178
+ .strict()
179
+ .readonly();
180
+ // The whole queue at once: the schedule page adds, renames, removes and reorders topics in
181
+ // place, and Undo puts the queue back as it was.
182
+ export const topicQueueSchema = z
183
+ .object({
184
+ baseVersion: z.number().int().positive(),
185
+ items: z.array(item).max(queueMax).readonly(),
186
+ })
187
+ .strict()
188
+ .readonly();
189
+ // The calendar: what will run, what is running or finished, and what waits in the batch queue.
190
+ // `topicSource` says where a run's topic comes from: the queue (`topic` names it), topics held
191
+ // for approval, a generation still to come, or none (the template as saved).
192
+ export const calendarRunSchema = z
193
+ .object({
194
+ at: z.string(),
195
+ scheduleId: id,
196
+ scheduleName: z.string(),
197
+ scheduleVersion: z.number().int().positive(),
198
+ paused: z.boolean(),
199
+ templateId: id,
200
+ templateVersion: z.number().int().positive(),
201
+ templateName: z.string().nullable(),
202
+ // The topic's position in the schedule's queue, for reordering; null without one.
203
+ index: z.number().int().nonnegative().nullable(),
204
+ topic: z.string().nullable(),
205
+ topicSource: z.enum(["queued", "held", "generated", "template"]),
206
+ // The project title this run will get, the way the scheduler builds it; null when it can't
207
+ // be known yet (a topic still to be approved or generated, or a deleted template). Older
208
+ // servers leave it out.
209
+ renderedTitle: z.string().nullable().default(null),
210
+ })
211
+ .strict()
212
+ .readonly();
213
+ export const calendarProjectSchema = z
214
+ .object({
215
+ id: z.string(),
216
+ title: z.string(),
217
+ state: z.enum(projectStates),
218
+ createdAt: z.string(),
219
+ finishedAt: z.string().nullable(),
220
+ scheduleId: z.string().nullable(),
221
+ // What the project waits on the person for; left out when nothing.
222
+ needs: z.enum(calendarNeeds).optional(),
223
+ // Finished, makes a video, not marked uploaded and not a bundled sample.
224
+ readyToUpload: z.literal(true).optional(),
225
+ // Stages waiting for a CLI plan's limits to reset.
226
+ limitWaits: z
227
+ .array(z
228
+ .object({
229
+ name: z.string(),
230
+ stage: z.enum(stageKinds),
231
+ resetsAt: z.string().nullable(),
232
+ retryAt: z.string(),
233
+ })
234
+ .strict()
235
+ .readonly())
236
+ .readonly()
237
+ .optional(),
238
+ })
239
+ .strict()
240
+ .readonly();
241
+ export const calendarQueuedSchema = z
242
+ .object({
243
+ projectId: z.string(),
244
+ title: z.string(),
245
+ batchId: z.string(),
246
+ position: z.number().int(),
247
+ state: z.enum(["queued", "active"]),
248
+ queuedAt: z.string(),
249
+ })
250
+ .strict()
251
+ .readonly();
252
+ export const calendarSchema = z
253
+ .object({
254
+ from: z.string(),
255
+ to: z.string(),
256
+ runs: z.array(calendarRunSchema).readonly(),
257
+ projects: z.array(calendarProjectSchema).readonly(),
258
+ queued: z.array(calendarQueuedSchema).readonly(),
259
+ })
260
+ .strict()
261
+ .readonly();
262
+ // A calendar covers at most this many days per request.
263
+ export const calendarMaxDays = 92;
264
+ export const topicTransferSchema = z
265
+ .object({
266
+ baseVersion: z.number().int().positive(),
267
+ index: z.number().int().nonnegative(),
268
+ targetId: id,
269
+ // Where it lands in the other schedule's queue; the end when left out.
270
+ position: z.number().int().nonnegative().optional(),
271
+ })
272
+ .strict()
273
+ .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,128 @@
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
+ // ("Cleopatra" and "Cleopatra's Palace" are one video; "Red Pyramids" and "Bent Pyramids" 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 ("History: Cleopatra"). A topic
12
+ // of one or two words is too easily found inside an unrelated title ("Pyramids" in "The Great
13
+ // Pyramids of Giza"), so one of the title's clauses must hold exactly its words, leaving aside
14
+ // question and framing words ("Hypatia" in "Who was Hypatia? The Last Scholar Explained").
15
+ export const jaccardMin = 0.6;
16
+ export const containmentMin = 0.5;
17
+ const filler = new Set([
18
+ "a",
19
+ "an",
20
+ "the",
21
+ "of",
22
+ "and",
23
+ "or",
24
+ "in",
25
+ "on",
26
+ "to",
27
+ "for",
28
+ "with",
29
+ "vs",
30
+ "s",
31
+ ]);
32
+ export function normaliseTopic(text) {
33
+ return text
34
+ .normalize("NFKD")
35
+ .replace(/\p{M}/gu, "")
36
+ .toLowerCase()
37
+ .replace(/&/g, " and ")
38
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
39
+ .trim();
40
+ }
41
+ export function topicWords(text) {
42
+ const words = normaliseTopic(text)
43
+ .split(" ")
44
+ .filter((word) => word !== "" && !filler.has(word))
45
+ .map((word) => word.length > 3 && word.endsWith("s") && !word.endsWith("ss") ? word.slice(0, -1) : word);
46
+ return new Set(words);
47
+ }
48
+ function shared(a, b) {
49
+ let count = 0;
50
+ for (const word of a)
51
+ if (b.has(word))
52
+ count += 1;
53
+ return count;
54
+ }
55
+ export function similarTopics(a, b) {
56
+ const left = normaliseTopic(a);
57
+ const right = normaliseTopic(b);
58
+ if (left === right)
59
+ return left !== "";
60
+ const wa = topicWords(a);
61
+ const wb = topicWords(b);
62
+ if (wa.size === 0 || wb.size === 0)
63
+ return false;
64
+ const common = shared(wa, wb);
65
+ if (common / (wa.size + wb.size - common) >= jaccardMin)
66
+ return true;
67
+ const small = Math.min(wa.size, wb.size);
68
+ const large = Math.max(wa.size, wb.size);
69
+ return common === small && small / large >= containmentMin;
70
+ }
71
+ export const shortTopicWords = 2;
72
+ // A topic already made as a project: all of its words appear in the project's title, or for a
73
+ // short topic, the title or one of its parts is near the same.
74
+ export function topicInTitle(topic, title) {
75
+ if (similarTopics(topic, title))
76
+ return true;
77
+ const words = topicWords(topic);
78
+ if (words.size === 0)
79
+ return false;
80
+ if (words.size <= shortTopicWords) {
81
+ const wanted = subjectWords(topic);
82
+ return titleClauses(title).some((clause) => sameWords(subjectWords(clause), wanted));
83
+ }
84
+ return shared(words, topicWords(title)) === words.size;
85
+ }
86
+ // Words that frame a subject in a title without being part of it.
87
+ const framing = new Set([
88
+ "who",
89
+ "what",
90
+ "why",
91
+ "how",
92
+ "when",
93
+ "where",
94
+ "which",
95
+ "is",
96
+ "was",
97
+ "are",
98
+ "were",
99
+ "really",
100
+ "explained",
101
+ ]);
102
+ function subjectWords(text) {
103
+ return new Set([...topicWords(text)].filter((word) => !framing.has(word)));
104
+ }
105
+ function sameWords(a, b) {
106
+ return a.size > 0 && a.size === b.size && shared(a, b) === a.size;
107
+ }
108
+ // "History: Cleopatra (Part 2) | Who is Hypatia?" → ["History", "Cleopatra", "Part 2", "Who is Hypatia"].
109
+ function titleClauses(title) {
110
+ return title
111
+ .split(/\s*[:|()[\]?!.,;\u2013\u2014]\s*|\s+-\s+/u)
112
+ .map((part) => part.trim())
113
+ .filter((part) => part !== "");
114
+ }
115
+ // Keeps the candidates that are new, in their order, dropping repeats among themselves too.
116
+ export function newTopics(candidates, known) {
117
+ const kept = [];
118
+ for (const candidate of candidates) {
119
+ if (known.topics.some((topic) => similarTopics(candidate, topic)))
120
+ continue;
121
+ if (known.projects.some((title) => topicInTitle(candidate, title)))
122
+ continue;
123
+ if (kept.some((topic) => similarTopics(candidate, topic)))
124
+ continue;
125
+ kept.push(candidate);
126
+ }
127
+ return kept;
128
+ }
@@ -0,0 +1,163 @@
1
+ import { parse, stringify } from "yaml";
2
+ import { detectSlots, render } from "../admission/substitute.js";
3
+ // The longest value one topic may give a keyword. Word counts, names and short notes fit;
4
+ // anything longer belongs in the template.
5
+ export const topicValueMax = 2000;
6
+ export const topicTitleMax = 200;
7
+ // A template's keywords: those its project title names plus those it stores a value for (Play
8
+ // stores one for every keyword its prompts use).
9
+ export function templateKeywords(form) {
10
+ return [...new Set([...detectSlots(form.title).names, ...Object.keys(form.values)])];
11
+ }
12
+ // The project title a run gets, exactly as the scheduler builds it: the template's values, the
13
+ // schedule's every-run values, the topic's own values, then the topic in its keyword. Without
14
+ // a topic keyword the topic is the whole title.
15
+ export function scheduledValues(form, schedule, topic) {
16
+ return {
17
+ ...form.values,
18
+ ...schedule.values,
19
+ ...(topic?.values ?? {}),
20
+ ...(topic !== undefined && schedule.topicKeyword !== null
21
+ ? { [schedule.topicKeyword]: topic.title }
22
+ : {}),
23
+ };
24
+ }
25
+ export function renderedTitle(form, schedule, topic) {
26
+ return topic !== undefined && schedule.topicKeyword === null
27
+ ? topic.title
28
+ : render(form.title, scheduledValues(form, schedule, topic));
29
+ }
30
+ const quote = (name) => `“${name}”`;
31
+ const listed = (names) => names.length === 0 ? "none" : names.map(quote).join(", ");
32
+ function rowName(index, title) {
33
+ return title === "" ? `Topic ${String(index + 1)}` : `Topic ${String(index + 1)} (${title})`;
34
+ }
35
+ // What is wrong with a queue, one plain sentence per problem naming the row and the key.
36
+ // `keywords` are the template's; `required` must be set on every row (keywords with no
37
+ // every-run value); an item equal to one in `kept` (the saved queue) is not checked again, so
38
+ // a topic saved before these rules keeps working until it is edited.
39
+ export function topicRowProblems(rows, options) {
40
+ const problems = [];
41
+ const kept = new Set((options.kept ?? []).map((row) => key(row)));
42
+ rows.forEach((row, index) => {
43
+ if (kept.has(key(row)))
44
+ return;
45
+ const name = rowName(index, row.title.trim());
46
+ if (row.title.trim() === "")
47
+ problems.push(options.topicKeyword === null
48
+ ? `${name} has no title. Write one, or remove the row.`
49
+ : `${name} has no ${quote(options.topicKeyword)}, the keyword each topic fills. Write one, or remove the row.`);
50
+ else if (row.title.trim().length > topicTitleMax)
51
+ problems.push(`${name} is ${String(row.title.trim().length)} characters; a topic can be at most ${String(topicTitleMax)}. Shorten it.`);
52
+ for (const [keyword, value] of Object.entries(row.values)) {
53
+ if (!options.keywords.includes(keyword))
54
+ problems.push(`${name}: ${quote(keyword)} is not a keyword of this template (its keywords are ${listed(options.keywords)}). Rename it to one of them or remove it.`);
55
+ else if (value.length > topicValueMax)
56
+ problems.push(`${name}: ${quote(keyword)} is ${String(value.length)} characters; a topic's value can be at most ${String(topicValueMax)}. Shorten it, or put the long text in the template.`);
57
+ }
58
+ for (const keyword of options.required ?? []) {
59
+ if (keyword === options.topicKeyword)
60
+ continue;
61
+ if ((row.values[keyword] ?? "").trim() === "")
62
+ problems.push(`${name} has no ${quote(keyword)}, and there is no every-run value to fall back on. Give this topic one, or fill ${quote(keyword)} for every run.`);
63
+ }
64
+ });
65
+ return problems;
66
+ }
67
+ function key(row) {
68
+ return JSON.stringify([
69
+ row.title,
70
+ Object.entries(row.values).sort(([a], [b]) => a.localeCompare(b)),
71
+ ]);
72
+ }
73
+ // One topic per line. A line whose text matches a row already in the queue keeps that row's
74
+ // values (first unused match), so switching to lines and back loses nothing.
75
+ export function rowsFromLines(text, previous) {
76
+ const unused = [...previous];
77
+ return text
78
+ .split("\n")
79
+ .map((line) => line.trim())
80
+ .filter((line) => line !== "")
81
+ .map((title) => {
82
+ const at = unused.findIndex((row) => row.title === title);
83
+ if (at === -1)
84
+ return { title, values: {} };
85
+ const [row] = unused.splice(at, 1);
86
+ return row ?? { title, values: {} };
87
+ });
88
+ }
89
+ export function linesFromRows(rows) {
90
+ return rows.map((row) => row.title).join("\n");
91
+ }
92
+ // The YAML / JSON list: each item is a map of keyword to value, or a plain string (the topic).
93
+ // JSON is YAML, so a JSON array reads the same way. Numbers and yes/no are read as the text
94
+ // they were written as; lists, maps and multi-document files are refused.
95
+ export function parseTopicList(text, options) {
96
+ if (text.trim() === "")
97
+ return { ok: true, rows: [] };
98
+ let data;
99
+ try {
100
+ // Every scalar as the string it was written as: "12000", "yes", "0012".
101
+ data = parse(text, { schema: "failsafe", uniqueKeys: true });
102
+ }
103
+ catch (error) {
104
+ const message = error instanceof Error ? error.message.split("\n")[0] : "";
105
+ return {
106
+ ok: false,
107
+ problems: [
108
+ `The list couldn't be read as YAML or JSON: ${message ?? ""}. Write one item per topic, such as "- Topic: Cleopatra".`,
109
+ ],
110
+ };
111
+ }
112
+ if (!Array.isArray(data))
113
+ return {
114
+ ok: false,
115
+ problems: [
116
+ 'The list must be a list of topics: start each one with "- " in YAML, or wrap them in [ ] in JSON.',
117
+ ],
118
+ };
119
+ const problems = [];
120
+ const rows = [];
121
+ data.forEach((item, index) => {
122
+ const name = `Topic ${String(index + 1)}`;
123
+ if (typeof item === "string") {
124
+ rows.push({ title: item.trim(), values: {} });
125
+ return;
126
+ }
127
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
128
+ problems.push(`${name} is not a topic or a set of keywords. Write it as "- Cleopatra" or "- Topic: Cleopatra".`);
129
+ return;
130
+ }
131
+ let title = "";
132
+ const values = {};
133
+ for (const [written, value] of Object.entries(item)) {
134
+ const keyword = written.trim();
135
+ // "Topic:" with nothing after it reads as no value.
136
+ const raw = value ?? "";
137
+ if (typeof raw !== "string") {
138
+ problems.push(`${name}: ${quote(keyword)} holds a list or a map. Give it one value, such as "${keyword}: 12000".`);
139
+ continue;
140
+ }
141
+ if (options.topicKeyword !== null && keyword === options.topicKeyword)
142
+ title = raw.trim();
143
+ else if (!options.keywords.includes(keyword))
144
+ problems.push(`${name}: ${quote(keyword)} is not a keyword of this template (its keywords are ${listed(options.keywords)}). Rename it to one of them or remove it.`);
145
+ else
146
+ values[keyword] = raw.trim();
147
+ }
148
+ rows.push({ title, values });
149
+ });
150
+ // Unknown keys were left out of the rows above, so these name the other problems once.
151
+ const all = [...problems, ...topicRowProblems(rows, options)];
152
+ return all.length > 0 ? { ok: false, problems: all } : { ok: true, rows };
153
+ }
154
+ // The queue as YAML: a plain "- title" when a topic sets nothing else, else a map with the
155
+ // topic keyword first. Reading it back with parseTopicList gives the same rows.
156
+ export function formatTopicList(rows, topicKeyword) {
157
+ if (rows.length === 0)
158
+ return "";
159
+ const items = rows.map((row) => Object.keys(row.values).length === 0 || topicKeyword === null
160
+ ? row.title
161
+ : { [topicKeyword]: row.title, ...row.values });
162
+ return stringify(items, { lineWidth: 0 });
163
+ }