@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
@@ -3,13 +3,15 @@ import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, rea
3
3
  import { join, relative, sep } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { subtitleModelDir } from "../../kernel/paths.js";
6
+ import { projectLanguage } from "../../kernel/ports/languages.js";
6
7
  import { projectById, setStageProgress } from "../admission/repo.js";
8
+ import { captionFont, captionFontDeps } from "../fonts/coverage.js";
7
9
  import { resolveFont } from "../fonts/index.js";
8
10
  import { outputPath, projectDir } from "../storage/layout.js";
9
11
  import { outputsOf } from "../storage/repo.js";
10
12
  import { spoken } from "../video/plan.js";
11
13
  import { captionCues, serializeAss, serializeSrt, serializeVtt } from "./captions.js";
12
- import { defaultSubtitles } from "./model.js";
14
+ import { defaultSubtitles, timingOperation } from "./model.js";
13
15
  import { spokenText } from "./transcript.js";
14
16
  export const subtitleRoles = [
15
17
  "subtitles_srt",
@@ -52,24 +54,27 @@ export async function prepareSubtitles(deps, context, audio, frame) {
52
54
  setStageProgress(deps.db, context.stage.id, 0, 100);
53
55
  context.emit({ type: "stage.progress", projectId, stage: "video", current: 0, total: 100 });
54
56
  if (audio.length === 0)
55
- throw new Error("Captions need narration audio, but this project has none. Turn captions off in Edit project → Subtitles, or turn narration on, then Retry stage.");
57
+ throw new Error("Captions need narration audio, but this project has none. Turn captions off in Edit project → Subtitles, or turn narration on, then Try again.");
56
58
  const outputs = outputsOf(deps.db, projectId);
57
59
  const segments = audio.map((segment) => ({
58
60
  ...segment,
59
61
  text: spoken(segment.kind) ? spokenText(deps, projectId, segment.kind, outputs) : "",
60
62
  }));
61
- const key = await timingKey(segments, context.signal);
63
+ const language = projectLanguage(project.config);
64
+ const key = await timingKey(segments, language, context.signal);
62
65
  const cache = readCache(deps, projectId, outputs);
63
66
  const dir = projectDir(deps.paths, projectId);
64
67
  const directory = mkdtempSync(join(dir, "captions-"));
65
68
  try {
66
- const font = await snapshotFont(deps, projectId, outputs, config.fontId, cache, directory);
69
+ const font = await snapshotFont(deps, projectId, outputs, config.fontId, cache, directory, language);
67
70
  const omissions = cache?.key === key ? [...cache.omissions] : [];
68
- const words = cache?.key === key ? cache.words : await alignSegments(deps, context, segments, omissions);
71
+ const words = cache?.key === key
72
+ ? cache.words
73
+ : await alignSegments(deps, context, segments, omissions, language);
69
74
  context.signal.throwIfAborted();
70
75
  const cues = captionCues(words);
71
76
  if (cues.length === 0)
72
- throw new Error("None of the narration could be matched to the article text, so captions can't be timed. Captions only work for English narration; if you uploaded your own audio, make sure it reads the article text, then Retry stage.");
77
+ throw new Error("None of the narration could be matched to the article text, so captions can't be timed. The narration must be in the project language set in Edit project → Language; if you uploaded your own audio, make sure it reads the article text, then Try again.");
73
78
  writeFileSync(join(directory, "subtitles.srt"), serializeSrt(cues), { mode: 0o600 });
74
79
  writeFileSync(join(directory, "subtitles.vtt"), serializeVtt(cues), { mode: 0o600 });
75
80
  writeFileSync(join(directory, "subtitles.ass"), serializeAss(cues, {
@@ -77,6 +82,8 @@ export async function prepareSubtitles(deps, context, audio, frame) {
77
82
  fontSize: config.fontSize,
78
83
  fontName: font.assName,
79
84
  position: config.position,
85
+ color: config.color,
86
+ outlineColor: config.outlineColor,
80
87
  }), { mode: 0o600 });
81
88
  writeFileSync(join(directory, "subtitles.json"), JSON.stringify({ key, words, font, omissions }), {
82
89
  mode: 0o600,
@@ -103,9 +110,10 @@ export async function prepareSubtitles(deps, context, audio, frame) {
103
110
  throw error;
104
111
  }
105
112
  }
106
- async function timingKey(segments, signal) {
113
+ async function timingKey(segments, language, signal) {
107
114
  // Bump when alignment normalization/model changes. Hash file contents, not timestamps.
108
- const hash = createHash("sha256").update("wav2vec2-en-a19f851-v2-omissions");
115
+ // English hashes the operation it always did (`timingOperation`).
116
+ const hash = createHash("sha256").update(timingOperation(language));
109
117
  for (const segment of segments) {
110
118
  signal.throwIfAborted();
111
119
  hash.update(JSON.stringify({ kind: segment.kind, seconds: segment.seconds, text: segment.text }));
@@ -117,9 +125,9 @@ async function timingKey(segments, signal) {
117
125
  }
118
126
  return hash.digest("hex");
119
127
  }
120
- async function alignSegments(deps, context, segments, omissions) {
128
+ async function alignSegments(deps, context, segments, omissions, language) {
121
129
  if (deps.alignSubtitles === undefined)
122
- throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
130
+ throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Try again; if it happens again, use Download diagnostics in Settings and report it.");
123
131
  const words = [];
124
132
  const total = segments.reduce((sum, segment) => sum + segment.seconds, 0);
125
133
  let offset = 0;
@@ -130,7 +138,8 @@ async function alignSegments(deps, context, segments, omissions) {
130
138
  audioPath: segment.path,
131
139
  onOmission: (omission) => omissions.push({ ...omission, start: omission.start + offset }),
132
140
  text: segment.text,
133
- cacheDir: subtitleModelDir(deps.paths.dataDir),
141
+ cacheDir: subtitleModelDir(deps.paths.dataDir, language),
142
+ ...(language === "en" ? {} : { language }),
134
143
  ffmpeg: deps.ffmpeg,
135
144
  signal: context.signal,
136
145
  onProgress: (current, maximum) => {
@@ -147,7 +156,7 @@ async function alignSegments(deps, context, segments, omissions) {
147
156
  });
148
157
  for (const word of aligned) {
149
158
  if (word.end > segment.seconds + 0.1)
150
- throw new Error("Caption timing came out longer than the narration audio. Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
159
+ throw new Error("Caption timing came out longer than the narration audio. Try again; if it happens again, use Download diagnostics in Settings and report it.");
151
160
  words.push({
152
161
  ...word,
153
162
  start: word.start + offset,
@@ -170,12 +179,14 @@ function readCache(deps, projectId, outputs) {
170
179
  return undefined;
171
180
  }
172
181
  }
173
- async function snapshotFont(deps, projectId, outputs, id, cache, directory) {
182
+ async function snapshotFont(deps, projectId, outputs, id, cache, directory, language) {
174
183
  const previous = outputs.find((one) => one.role === "subtitle_font");
175
184
  const previousPath = previous === undefined ? undefined : outputPath(deps.paths, projectId, previous.path);
176
- const saved = cache?.font.id === id && previousPath !== undefined && existsSync(previousPath)
177
- ? { ...cache.font, path: previousPath }
178
- : await resolveFont(deps.paths, id);
185
+ const saved = language !== "en"
186
+ ? await captionFont(captionFontDeps(deps.paths), id, language)
187
+ : cache?.font.id === id && previousPath !== undefined && existsSync(previousPath)
188
+ ? { ...cache.font, path: previousPath }
189
+ : await resolveFont(deps.paths, id);
179
190
  mkdirSync(join(directory, "fonts"), { mode: 0o700 });
180
191
  copyFileSync(saved.path, join(directory, "fonts", `selected${saved.extension}`));
181
192
  return { id: saved.id, name: saved.name, assName: saved.assName, extension: saved.extension };
@@ -27,7 +27,7 @@ export function spokenText(deps, projectId, kind, outputs) {
27
27
  return segment.text;
28
28
  }
29
29
  }
30
- throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, and captions need the text spoken in the narration. Use Re-run section on Article, then Retry stage.`);
30
+ throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, and captions need the text spoken in the narration. Use More → Write the article again in the Article section, then Try again.`);
31
31
  }
32
32
  function payload(raw) {
33
33
  return textPayload.parse(JSON.parse(raw ?? "null"));
@@ -22,13 +22,13 @@ export async function runThumbnail(deps, context, providers) {
22
22
  if (source !== "from_prompt" && source !== "prompt_by_llm") {
23
23
  // Off is `skipped` and Provide is `provided` at project creation,
24
24
  // so the runner never starts this stage for either.
25
- throw new Error(`Slopify hit an internal error (the thumbnail stage cannot run with its source set to ${source}). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
25
+ throw new Error(`Slopify hit an internal error (the thumbnail stage cannot run with its source set to ${source}). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
26
26
  }
27
27
  const choice = project.config.images;
28
28
  if (choice === undefined) {
29
29
  // Admission requires the image provider whenever the thumbnail is generated, so reaching
30
30
  // here is a bug in admission rather than the user's.
31
- throw new Error("No image model is set for the thumbnail. Choose one in Edit project → Providers, then Retry stage.");
31
+ throw new Error("No image model is set for the thumbnail. Choose one in Edit project → Providers, then Try again.");
32
32
  }
33
33
  const result = source === "from_prompt" ? undefined : await byLlm(deps, context, providers, project);
34
34
  if (result !== undefined && !result.ok)
@@ -60,7 +60,7 @@ export async function runThumbnail(deps, context, providers) {
60
60
  function fromTemplate(project) {
61
61
  const rendered = project.config.rendered.thumbnailPrompt;
62
62
  if (rendered === undefined) {
63
- throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
63
+ throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Try again; if it happens again, use Download diagnostics in Settings and report it.");
64
64
  }
65
65
  return rendered;
66
66
  }
@@ -83,7 +83,7 @@ async function byLlm(deps, context, providers, project) {
83
83
  }
84
84
  if (llm === undefined) {
85
85
  // Admission requires the LLM row when the thumbnail source is Prompt by LLM.
86
- throw new Error("No AI model is set for writing the thumbnail prompt. Choose one in Edit project → Providers, then Retry stage.");
86
+ throw new Error("No AI model is set for writing the thumbnail prompt. Choose one in Edit project → Providers, then Try again.");
87
87
  }
88
88
  const messages = thumbnailMessages(brief(deps, project));
89
89
  const answer = await providers.llm({
@@ -117,7 +117,7 @@ function named(choice) {
117
117
  function brief(deps, project) {
118
118
  const instruction = project.config.rendered.thumbnailPrompt;
119
119
  if (instruction === undefined) {
120
- throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
120
+ throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Try again; if it happens again, use Download diagnostics in Settings and report it.");
121
121
  }
122
122
  return {
123
123
  instruction,
@@ -133,7 +133,7 @@ function brief(deps, project) {
133
133
  function articleText(deps, projectId) {
134
134
  const article = outputsOf(deps.db, projectId).find((output) => output.role === "article_txt");
135
135
  if (article === undefined) {
136
- throw new Error("The thumbnail prompt is written from the article, but the article isn't ready. Let the Article stage finish, then Retry stage.");
136
+ throw new Error("The thumbnail prompt is written from the article, but the article isn't ready. Let the Article stage finish, then Try again.");
137
137
  }
138
138
  return readFileSync(outputPath(deps.paths, projectId, article.path), "utf8");
139
139
  }
@@ -212,7 +212,7 @@ function instructionsText(messages) {
212
212
  }
213
213
  function payloadOf(piece) {
214
214
  if (piece.payload === null) {
215
- throw new Error("Slopify hit an internal error (the saved thumbnail prompt is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
215
+ throw new Error("Slopify hit an internal error (the saved thumbnail prompt is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
216
216
  }
217
217
  return writtenPayload.parse(JSON.parse(piece.payload));
218
218
  }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ // Settings → Trash. Deleting a project, a Library prompt or intro/outro, a template or a
3
+ // schedule stamps it instead of removing it; it stays here this long, then the daily purge
4
+ // removes it for good (a project's folder with it).
5
+ export const trashDays = 30;
6
+ export const trashMs = trashDays * 24 * 60 * 60_000;
7
+ export const trashKinds = ["project", "prompt", "entry", "template", "schedule"];
8
+ export const trashItemSchema = z.object({
9
+ kind: z.enum(trashKinds),
10
+ id: z.string(),
11
+ name: z.string(),
12
+ // What sort of prompt ("article", "image"…) or entry ("intro", "outro"); null otherwise.
13
+ detail: z.string().nullable(),
14
+ deletedAt: z.string(),
15
+ // When the daily purge removes it for good.
16
+ purgeAt: z.string(),
17
+ daysLeft: z.number().int().nonnegative(),
18
+ });
19
+ export const restoredSchema = z.object({
20
+ kind: z.enum(trashKinds),
21
+ id: z.string(),
22
+ name: z.string(),
23
+ // The name it came back under when a live item had taken the old one meanwhile.
24
+ renamedFrom: z.string().nullable(),
25
+ });
@@ -0,0 +1,273 @@
1
+ import { z } from "zod";
2
+ import { transact } from "../../kernel/db/tx.js";
3
+ import { deleteVersions, recordVersion } from "../library/history.js";
4
+ import { nameMax } from "../library/model.js";
5
+ import { entryById, promptById } from "../library/repo.js";
6
+ import { deleteProject, projectBusy } from "../storage/delete-project.js";
7
+ import { trashKinds, trashMs, } from "./model.js";
8
+ // Settings → Trash: what was deleted in the last 30 days, put back or removed for good.
9
+ //
10
+ // Each kind keeps its stamp where it lives: a project in `project_trash` (the projects table is
11
+ // read positionally in many places), a prompt, entry or template in its own `deleted_at`, and a
12
+ // schedule in the `deleted_at` it always had as the tombstone that keeps its run history. A
13
+ // schedule leaves the trash by getting `purged_at`; its row and history stay, as before.
14
+ const dayMs = 24 * 60 * 60_000;
15
+ // A template's name column allows this much (0008's CHECK on project_template_revisions).
16
+ const templateNameMax = 200;
17
+ const row = z.object({
18
+ id: z.string(),
19
+ name: z.string(),
20
+ detail: z.string().nullable(),
21
+ deleted_at: z.string(),
22
+ });
23
+ // Every kind in one statement, newest deletion first.
24
+ const trashedRows = `
25
+ SELECT 'project' AS kind, p.id, p.title AS name, NULL AS detail, t.deleted_at
26
+ FROM project_trash t JOIN projects p ON p.id = t.project_id
27
+ UNION ALL SELECT 'prompt', id, name, kind, deleted_at FROM prompts WHERE deleted_at IS NOT NULL
28
+ UNION ALL SELECT 'entry', id, name, category, deleted_at FROM entries WHERE deleted_at IS NOT NULL
29
+ UNION ALL SELECT 'template', t.id, r.name, NULL, t.deleted_at
30
+ FROM project_templates t
31
+ JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
32
+ WHERE t.deleted_at IS NOT NULL
33
+ UNION ALL SELECT 'schedule', id, name, NULL, deleted_at FROM schedules
34
+ WHERE deleted_at IS NOT NULL AND purged_at IS NULL`;
35
+ export function listTrash(deps) {
36
+ const now = deps.clock.now().valueOf();
37
+ return deps.db
38
+ .prepare(`SELECT * FROM (${trashedRows}) ORDER BY deleted_at DESC, kind, id`)
39
+ .all()
40
+ .map((raw) => {
41
+ const parsed = row.extend({ kind: z.enum(trashKinds) }).parse(raw);
42
+ const purgeAt = Date.parse(parsed.deleted_at) + trashMs;
43
+ return {
44
+ kind: parsed.kind,
45
+ id: parsed.id,
46
+ name: parsed.name,
47
+ detail: parsed.detail,
48
+ deletedAt: parsed.deleted_at,
49
+ purgeAt: new Date(purgeAt).toISOString(),
50
+ daysLeft: Math.max(0, Math.ceil((purgeAt - now) / dayMs)),
51
+ };
52
+ });
53
+ }
54
+ // 07 Projects' Delete. Refused while the run is going, as removing it always was: the work
55
+ // has to be stopped by the user first. A project waiting its turn (a queued batch item, a
56
+ // checkpoint, work left over from a restart) simply stops being picked up: the runner holds a
57
+ // project in the trash like a paused one and the batch queue moves on past it.
58
+ export function trashProject(deps, projectId) {
59
+ return transact(deps.db, () => {
60
+ const found = deps.db
61
+ .prepare("SELECT (SELECT 1 FROM project_trash t WHERE t.project_id = p.id) AS trashed FROM projects p WHERE p.id = ?")
62
+ .get(projectId);
63
+ if (found === undefined || found.trashed === 1)
64
+ return { ok: false, reason: "not-found" };
65
+ if (projectBusy(deps, projectId))
66
+ return { ok: false, reason: "running" };
67
+ deps.db
68
+ .prepare("INSERT INTO project_trash (project_id, deleted_at) VALUES (?, ?)")
69
+ .run(projectId, deps.clock.now().toISOString());
70
+ return { ok: true, value: null };
71
+ });
72
+ }
73
+ // Puts the item back where it was. A prompt, entry or template whose name a live one has
74
+ // taken meanwhile comes back as "Name (restored)", then "Name (restored 2)"…
75
+ //
76
+ // A schedule comes back paused with no next run, whatever it was when deleted (only a
77
+ // canceled or completed one can be deleted): nothing runs until Resume is pressed on
78
+ // Schedules, which picks the next occurrence from then. Its template must be live: one in the
79
+ // trash is restored first, and one removed for good leaves the schedule nothing to run.
80
+ export function restoreItem(deps, kind, id) {
81
+ return transact(deps.db, () => {
82
+ switch (kind) {
83
+ case "project":
84
+ return restoreProject(deps.db, id);
85
+ case "prompt":
86
+ return restoreLibrary(deps, "prompt", id);
87
+ case "entry":
88
+ return restoreLibrary(deps, "entry", id);
89
+ case "template":
90
+ return restoreTemplate(deps, id);
91
+ case "schedule":
92
+ return restoreSchedule(deps, id);
93
+ }
94
+ });
95
+ }
96
+ function restoreProject(db, id) {
97
+ const found = db
98
+ .prepare("SELECT p.title FROM project_trash t JOIN projects p ON p.id = t.project_id WHERE t.project_id = ?")
99
+ .get(id);
100
+ if (typeof found?.title !== "string")
101
+ return { ok: false, reason: "not-found" };
102
+ db.prepare("DELETE FROM project_trash WHERE project_id = ?").run(id);
103
+ return { ok: true, value: { kind: "project", id, name: found.title, renamedFrom: null } };
104
+ }
105
+ function restoreLibrary(deps, itemKind, id) {
106
+ const table = itemKind === "prompt" ? "prompts" : "entries";
107
+ const group = itemKind === "prompt" ? "kind" : "category";
108
+ const found = deps.db
109
+ .prepare(`SELECT name, ${group} AS grp FROM ${table} WHERE id = ? AND deleted_at IS NOT NULL`)
110
+ .get(id);
111
+ if (typeof found?.name !== "string" || typeof found.grp !== "string")
112
+ return { ok: false, reason: "not-found" };
113
+ const taken = deps.db.prepare(`SELECT 1 FROM ${table} WHERE ${group} = ? AND lower(name) = lower(?) AND deleted_at IS NULL`);
114
+ const grp = found.grp;
115
+ const name = freeName(found.name, nameMax, (candidate) => taken.get(grp, candidate) !== undefined);
116
+ const renamed = name !== found.name;
117
+ deps.db
118
+ .prepare(`UPDATE ${table} SET deleted_at = NULL, name = ?, updated_at = ${renamed ? "?" : "updated_at"} WHERE id = ?`)
119
+ .run(...(renamed ? [name, deps.clock.now().toISOString(), id] : [name, id]));
120
+ // A rename shows in History as a version of its own.
121
+ if (renamed) {
122
+ const item = itemKind === "prompt" ? promptById(deps.db, id) : entryById(deps.db, id);
123
+ if (item !== undefined)
124
+ recordVersion(deps.db, itemKind, item);
125
+ }
126
+ return {
127
+ ok: true,
128
+ value: { kind: itemKind, id, name, renamedFrom: renamed ? found.name : null },
129
+ };
130
+ }
131
+ function restoreTemplate(deps, id) {
132
+ const found = deps.db
133
+ .prepare(`SELECT r.name, r.version, r.document_json FROM project_templates t
134
+ JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
135
+ WHERE t.id = ? AND t.deleted_at IS NOT NULL`)
136
+ .get(id);
137
+ if (typeof found?.name !== "string" ||
138
+ typeof found.version !== "number" ||
139
+ typeof found.document_json !== "string")
140
+ return { ok: false, reason: "not-found" };
141
+ const taken = deps.db.prepare(`SELECT 1 FROM project_templates t
142
+ JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
143
+ WHERE t.deleted_at IS NULL AND lower(r.name) = lower(?)`);
144
+ const name = freeName(found.name, templateNameMax, (candidate) => taken.get(candidate) !== undefined);
145
+ deps.db.prepare("UPDATE project_templates SET deleted_at = NULL WHERE id = ?").run(id);
146
+ // A rename is a new version, as a rename in the template editor is; the versions a schedule
147
+ // or a draft names stay as they were.
148
+ if (name !== found.name) {
149
+ const version = found.version + 1;
150
+ deps.db
151
+ .prepare("INSERT INTO project_template_revisions(template_id,version,name,document_json,created_at) VALUES (?,?,?,?,?)")
152
+ .run(id, version, name, found.document_json, deps.clock.now().toISOString());
153
+ deps.db.prepare("UPDATE project_templates SET head_version = ? WHERE id = ?").run(version, id);
154
+ }
155
+ return {
156
+ ok: true,
157
+ value: {
158
+ kind: "template",
159
+ id,
160
+ name,
161
+ renamedFrom: name === found.name ? null : found.name,
162
+ },
163
+ };
164
+ }
165
+ function restoreSchedule(deps, id) {
166
+ const found = deps.db
167
+ .prepare(`SELECT s.name, s.template_id,
168
+ (SELECT t.deleted_at IS NOT NULL FROM project_templates t WHERE t.id = s.template_id) AS trashed
169
+ FROM schedules s WHERE s.id = ? AND s.deleted_at IS NOT NULL AND s.purged_at IS NULL`)
170
+ .get(id);
171
+ if (typeof found?.name !== "string")
172
+ return { ok: false, reason: "not-found" };
173
+ if (found.trashed === null)
174
+ return { ok: false, reason: "template-gone" };
175
+ if (found.trashed === 1)
176
+ return { ok: false, reason: "template-in-trash" };
177
+ const at = deps.clock.now().toISOString();
178
+ deps.db
179
+ .prepare(`UPDATE schedules SET deleted_at = NULL, status = 'paused', next_run_at = NULL,
180
+ version = version + 1, updated_at = ? WHERE id = ?`)
181
+ .run(at, id);
182
+ return { ok: true, value: { kind: "schedule", id, name: found.name, renamedFrom: null } };
183
+ }
184
+ // "Delete now": removes one item for good. A project's folder goes first, then its rows
185
+ // (`slices/storage/delete-project.ts`); a folder that will not go leaves it in the trash.
186
+ export function deleteNow(deps, kind, id) {
187
+ switch (kind) {
188
+ case "project": {
189
+ if (deps.db.prepare("SELECT 1 FROM project_trash WHERE project_id = ?").get(id) === undefined)
190
+ return { ok: false, reason: "not-found" };
191
+ const removed = deleteProject(deps, id);
192
+ if (removed.ok)
193
+ return { ok: true, value: null };
194
+ return {
195
+ ok: false,
196
+ reason: removed.reason === "no-project" ? "not-found" : removed.reason,
197
+ ...(removed.detail === undefined ? {} : { detail: removed.detail }),
198
+ };
199
+ }
200
+ case "prompt":
201
+ case "entry":
202
+ return transact(deps.db, () => {
203
+ const table = kind === "prompt" ? "prompts" : "entries";
204
+ const changed = deps.db
205
+ .prepare(`DELETE FROM ${table} WHERE id = ? AND deleted_at IS NOT NULL`)
206
+ .run(id).changes;
207
+ if (Number(changed) === 0)
208
+ return { ok: false, reason: "not-found" };
209
+ deleteVersions(deps.db, kind, id);
210
+ return { ok: true, value: null };
211
+ });
212
+ case "template": {
213
+ // Revisions cascade. A schedule in the trash may still name it: that schedule can then
214
+ // no longer be restored (`restoreSchedule`), only removed.
215
+ const changed = deps.db
216
+ .prepare("DELETE FROM project_templates WHERE id = ? AND deleted_at IS NOT NULL")
217
+ .run(id).changes;
218
+ return Number(changed) === 0 ? { ok: false, reason: "not-found" } : { ok: true, value: null };
219
+ }
220
+ case "schedule": {
221
+ // Gone from the trash; the row stays as the history Schedules lists under Deleted.
222
+ const changed = deps.db
223
+ .prepare("UPDATE schedules SET purged_at = ? WHERE id = ? AND deleted_at IS NOT NULL AND purged_at IS NULL")
224
+ .run(deps.clock.now().toISOString(), id).changes;
225
+ return Number(changed) === 0 ? { ok: false, reason: "not-found" } : { ok: true, value: null };
226
+ }
227
+ }
228
+ }
229
+ // The daily purge: everything deleted more than 30 days ago goes for good. A failure on one
230
+ // item is logged and leaves it for the next pass; the rest still go.
231
+ export function purgeExpired(deps) {
232
+ const cutoff = new Date(deps.clock.now().valueOf() - trashMs).toISOString();
233
+ let removed = 0;
234
+ let kept = 0;
235
+ for (const item of listTrash(deps)) {
236
+ if (item.deletedAt > cutoff)
237
+ continue;
238
+ const result = deleteNow(deps, item.kind, item.id);
239
+ if (result.ok)
240
+ removed++;
241
+ else {
242
+ kept++;
243
+ deps.log.write("warn", "trash.purge", {
244
+ ...(item.kind === "project" ? { projectId: item.id } : {}),
245
+ detail: `${item.kind} ${item.id}: ${result.detail ?? result.reason}`,
246
+ });
247
+ }
248
+ }
249
+ return { removed, kept };
250
+ }
251
+ export function createTrashPurge(deps) {
252
+ let last;
253
+ return {
254
+ tick: () => {
255
+ const now = deps.clock.now().valueOf();
256
+ if (last !== undefined && now - last < dayMs)
257
+ return undefined;
258
+ last = now;
259
+ return purgeExpired(deps);
260
+ },
261
+ };
262
+ }
263
+ // The name itself when free, else "Name (restored)", "Name (restored 2)"… cut to fit `max`.
264
+ export function freeName(name, max, taken) {
265
+ if (!taken(name))
266
+ return name;
267
+ for (let n = 1;; n++) {
268
+ const suffix = n === 1 ? " (restored)" : ` (restored ${String(n)})`;
269
+ const candidate = `${name.slice(0, Math.max(1, max - suffix.length)).trimEnd()}${suffix}`;
270
+ if (!taken(candidate))
271
+ return candidate;
272
+ }
273
+ }
@@ -0,0 +1,57 @@
1
+ // The tutorials are the GitHub wiki's pages (docs/wiki), so their links name sections the way
2
+ // GitHub does: `[text](Page-Name#section-anchor)`. These helpers work out those anchors, for
3
+ // the server's search, the web page that scrolls to them and the test that checks every help
4
+ // entry's Learn more link. Pure: the web bundle imports this file.
5
+ export const tutorialPageIdPattern = /^[A-Za-z0-9][A-Za-z0-9-]{0,79}$/;
6
+ // A heading's text as GitHub renders it: link targets, code ticks and emphasis marks dropped.
7
+ export function headingText(markdown) {
8
+ return markdown
9
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
10
+ .replace(/`([^`]*)`/g, "$1")
11
+ .replace(/(\*\*|__)(.+?)\1/g, "$2")
12
+ .replace(/(^|[^\p{L}\p{N}])[*_]([^*_]+)[*_](?![\p{L}\p{N}])/gu, "$1$2")
13
+ .replace(/<[^>]+>/g, "")
14
+ .trim();
15
+ }
16
+ // GitHub's anchor for a heading's text: lower case, punctuation dropped, each space a hyphen.
17
+ export function githubSlug(text) {
18
+ return text
19
+ .toLowerCase()
20
+ .replace(/[^\p{L}\p{N}\p{M} _-]/gu, "")
21
+ .replace(/ /g, "-");
22
+ }
23
+ const headingLine = /^ {0,3}(#{1,6})\s+(.+?)\s*#*\s*$/;
24
+ // Every heading of a page in order, with GitHub's anchor; a repeated anchor gets `-1`, `-2`.
25
+ // `#` inside fenced code is not a heading.
26
+ export function tutorialHeadings(markdown) {
27
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
28
+ const seen = new Map();
29
+ const headings = [];
30
+ let fenced = false;
31
+ for (const [line, raw] of lines.entries()) {
32
+ if (/^\s*(```|~~~)/.test(raw))
33
+ fenced = !fenced;
34
+ const match = fenced ? null : headingLine.exec(raw);
35
+ if (match === null)
36
+ continue;
37
+ const text = headingText(match[2] ?? "");
38
+ const base = githubSlug(text);
39
+ const count = seen.get(base) ?? 0;
40
+ seen.set(base, count + 1);
41
+ headings.push({
42
+ level: (match[1] ?? "#").length,
43
+ text,
44
+ anchor: count === 0 ? base : `${base}-${String(count)}`,
45
+ line,
46
+ });
47
+ }
48
+ return headings;
49
+ }
50
+ // The anchors a page offers, for checking a link.
51
+ export function tutorialAnchors(markdown) {
52
+ return new Set(tutorialHeadings(markdown).map((heading) => heading.anchor));
53
+ }
54
+ // A page id as a title when the sidebar gives none: "Play-Overview" → "Play Overview".
55
+ export function pageTitleOf(id) {
56
+ return id.replace(/-/g, " ");
57
+ }