@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,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { chmod, lchown, lstat, mkdir, readdir, rm, writeFile } from "node:fs/promises";
2
+ import { chmod, lchown, lstat, mkdir, readdir, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { isMissing, syncDirectory, treeDigest } from "./tree.js";
@@ -78,10 +78,6 @@ export async function volumeOperation(args) {
78
78
  await lchown("/data/home", uid, gid);
79
79
  return { ok: true };
80
80
  }
81
- if (operation === "probe") {
82
- await writeFile("/probe/container", "slopify ownership probe", { flag: "wx", mode: 0o600 });
83
- return { uid: process.getuid?.(), gid: process.getgid?.() };
84
- }
85
81
  throw new Error("Unknown fixed Docker volume operation.");
86
82
  }
87
83
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
@@ -6,16 +6,21 @@ import { installHostPackage } from "../host-cli/install.js";
6
6
  import { ensureBridgeToken, hasCode, prepareHostPaths } from "../host-cli/paths.js";
7
7
  import { ensureHostService, privateRead, privateWrite } from "../host-cli/service.js";
8
8
  import { hostCommandName, resolveHostCommand } from "../host-cli/status.js";
9
- import { hostCliProtocol, hostLlmIds } from "../kernel/ports/host-cli.js";
9
+ import { hostLlmIds } from "../kernel/ports/host-cli.js";
10
10
  export function assertManagedDockerHost(platform, uid, gid) {
11
11
  if (platform !== "linux")
12
- throw new Error("The --docker launcher only works on Linux. On this system, run Slopify without Docker (npx @gentbajko/slopify), or start the Slopify Docker image yourself with docker run.");
12
+ throw new Error("The --docker install only works on Linux. On this system, run Slopify without Docker (npx @gentbajko/slopify), or run the compose.yaml from the Slopify repository yourself (docker compose up -d).");
13
13
  if (uid === undefined || gid === undefined || uid === 0)
14
14
  throw new Error("Don't run the --docker launcher with sudo or as root. Run it as your normal user, and if Docker then says permission denied, give your user access: sudo usermod -aG docker $USER, then log out and back in.");
15
15
  }
16
- export async function prepareDockerHostCli(options) {
16
+ /**
17
+ * Decides whether the Docker installation uses this machine's AI CLIs, asking once for consent,
18
+ * before anything changes. `ensure` then installs or updates the bridge: a private helper, run as
19
+ * the systemd user service slopify-cli-bridge.service, at the same version as the image.
20
+ */
21
+ export async function planDockerHostCli(options) {
17
22
  if (options.disabled)
18
- return {};
23
+ return { enabled: false };
19
24
  let installed = false;
20
25
  for (const id of hostLlmIds) {
21
26
  try {
@@ -28,7 +33,7 @@ export async function prepareDockerHostCli(options) {
28
33
  }
29
34
  }
30
35
  if (!installed)
31
- return {};
36
+ return { enabled: false };
32
37
  if (process.platform !== "linux")
33
38
  throw new Error("Using this machine's AI CLIs from Docker needs Linux with systemd. Start with --host-cli=off to use API keys only.");
34
39
  const uid = process.getuid?.();
@@ -53,25 +58,12 @@ export async function prepareDockerHostCli(options) {
53
58
  if (!approved)
54
59
  throw new Error("You declined, so Slopify did not set up your host AI CLIs and nothing was changed. Run the launcher again and answer y to allow it, or add --host-cli=off to start with API keys only.");
55
60
  }
56
- const inspect = () => options.runner.exec("docker", [
57
- "image",
58
- "inspect",
59
- "--format",
60
- '{{index .Config.Labels "io.slopify.host-cli-protocol"}}',
61
- options.image,
62
- ], options.signal);
63
- let image = await inspect();
64
- if (image.code !== 0 || image.stdout.trim() !== String(hostCliProtocol)) {
65
- const pulled = await options.runner.exec("docker", ["pull", options.image], options.signal);
66
- if (pulled.code !== 0)
67
- throw new Error(`Could not download the Slopify Docker image ${options.image}. Check your internet connection and that Docker is running (docker info), then try again. Nothing was changed.`);
68
- image = await inspect();
69
- if (image.code !== 0 || image.stdout.trim() !== String(hostCliProtocol))
70
- throw new Error(`The Docker image ${options.image} is too old to use the AI CLIs on this machine. Use the latest image (unset SLOPIFY_DOCKER_IMAGE, or point it to a current release) and try again, or start with --host-cli=off.`);
71
- }
72
61
  const supported = await options.runner.exec("systemctl", ["--user", "list-units", "--no-legend", "--no-pager", "slopify-cli-bridge.service"], options.signal);
73
62
  if (supported.code !== 0)
74
63
  throw new Error("Using this machine's AI CLIs from Docker needs systemd user services (systemctl --user), which aren't available here. Start with --host-cli=off to use API keys only, or run Slopify without Docker: npx @gentbajko/slopify.");
64
+ return { enabled: true, ensure: () => ensureDockerHostCli(options, uid, receiptPath) };
65
+ }
66
+ async function ensureDockerHostCli(options, uid, receiptPath) {
75
67
  const paths = await prepareHostPaths(options.root);
76
68
  const lock = join(paths.root, "setup.lock");
77
69
  const deadline = Date.now() + 30_000;
@@ -107,7 +99,7 @@ export async function prepareDockerHostCli(options) {
107
99
  env: options.env,
108
100
  });
109
101
  await privateWrite(receiptPath, JSON.stringify({ version: 1, automaticStartup: true }));
110
- return { directory: paths.share };
102
+ return paths.share;
111
103
  }
112
104
  finally {
113
105
  await rmdir(lock);
@@ -72,7 +72,9 @@ export function createHub(deps) {
72
72
  for (const subscriber of projects.get(projectId) ?? []) {
73
73
  send(subscriber, event);
74
74
  }
75
- if (event.type === "project.state" || event.type === "project.updated") {
75
+ if (event.type === "project.state" ||
76
+ event.type === "project.updated" ||
77
+ event.type === "review.flagged") {
76
78
  for (const subscriber of globals)
77
79
  send(subscriber, event);
78
80
  }
@@ -12,7 +12,7 @@ import { startHostServer } from "../host-cli/server.js";
12
12
  import { resolveHostCommand } from "../host-cli/status.js";
13
13
  import { readVersion } from "../kernel/version.js";
14
14
  import { nodeCliProbe } from "../slices/settings/cli-status.js";
15
- import { dockerRoot } from "./docker-projects/state.js";
15
+ import { dockerRoot } from "./docker-install/state.js";
16
16
  const { values } = parseArgs({ options: { "state-dir": { type: "string" } }, strict: true });
17
17
  if (!values["state-dir"])
18
18
  throw new Error("A host helper state directory is required.");
@@ -11,6 +11,7 @@ import { revisionControlSchema, } from "../../slices/control/revision-control-sc
11
11
  import { recoverProject } from "../../slices/rebuild/recovery.js";
12
12
  import { recoveryResultSchema } from "../../slices/rebuild/recovery-model.js";
13
13
  import { resumable } from "../../slices/rebuild/recovery-repo.js";
14
+ import { clearSoftening, refusedKeys, requestSoftening } from "../../slices/rebuild/soften.js";
14
15
  import { adoptBaseline } from "../../slices/revisions/adopt.js";
15
16
  import { currentRevisionId } from "../../slices/revisions/repo.js";
16
17
  import { allowsCustomModel } from "../../slices/settings/models.js";
@@ -59,7 +60,12 @@ export function actionRoutes(deps) {
59
60
  ...reruns,
60
61
  runner: deps.runner,
61
62
  emit: (projectId, event) => deps.hub.emit(projectId, event),
62
- providers: () => providerStatuses({ db: deps.db, probe: deps.probe, hostCliStatus: deps.hostCliStatus }),
63
+ providers: () => providerStatuses({
64
+ db: deps.db,
65
+ probe: deps.probe,
66
+ hostCliStatus: deps.hostCliStatus,
67
+ host: deps.speechHost,
68
+ }),
63
69
  allowsCustomModels: deps.catalogue ? () => false : allowsCustomModel,
64
70
  modelsFor: deps.modelsFor ?? (() => Promise.reject(new Error("Model catalog unavailable"))),
65
71
  };
@@ -99,7 +105,7 @@ export function actionRoutes(deps) {
99
105
  "revision-required": "This page is out of date. Reload the page, then try again.",
100
106
  conflict: "This project changed since the page loaded. Reload the page to see the latest, then try again.",
101
107
  "idempotency-conflict": "This action was already sent with different details. Reload the page, then try again.",
102
- "rebuild-required": "This change needs some outputs to be made again. In the Edit tab, use Rebuild affected outputs to see what will be regenerated and what it costs.",
108
+ "rebuild-required": "This change needs some outputs to be made again. In the Edit tab, use Choose what to remake (in the project's More menu) to see what will be regenerated and what it costs.",
103
109
  };
104
110
  return problem(c, {
105
111
  status: codes[result.reason],
@@ -130,14 +136,14 @@ export function actionRoutes(deps) {
130
136
  const messages = {
131
137
  "no-project": "This project no longer exists. Go back to Projects to pick another.",
132
138
  "no-revision": "The project's saved settings changed since the page loaded. Reload the page, then try again.",
133
- conflict: "This project changed since the page loaded. Reload the page to see the latest, then use Resume again.",
139
+ conflict: "This project changed since the page loaded. Reload the page to see the latest, then use Continue the run again.",
134
140
  "idempotency-conflict": "This action was already sent with different details. Reload the page, then try again.",
135
141
  "invalid-edit": "This section has settings that need fixing first. Open the Edit tab, choose Edit project and correct this section, then try again.",
136
- "stale-preview": "What needs to be made changed since the page loaded. Reload the page, then use Resume again.",
142
+ "stale-preview": "What needs to be made changed since the page loaded. Reload the page, then use Continue the run again.",
137
143
  "invalid-selection": "This section has nothing generated to re-run because its content was supplied by you. To change it, open the Edit tab and choose Edit project.",
138
- "review-required": "Content you supplied or captions you edited by hand would be replaced. Change them in the Edit tab with Edit project, or use Rebuild affected outputs to confirm the replacement.",
139
- "cost-ack-required": "This re-run needs you to review its cost first. In the Edit tab, use Rebuild affected outputs.",
140
- readiness: "Slopify cannot run this yet. Check the provider, model, voice and any files you supplied, then use Resume.",
144
+ "review-required": "Content you supplied or captions you edited by hand would be replaced. Change them in the Edit tab with Edit project, or use Choose what to remake (in the project's More menu) to confirm the replacement.",
145
+ "cost-ack-required": "This re-run needs you to review its cost first. In the Edit tab, use Choose what to remake (in the project's More menu).",
146
+ readiness: "Slopify cannot run this yet. Check the provider, model, voice and any files you supplied, then use Continue the run.",
141
147
  running: "This section is still running. Wait for it to finish, or Pause the project, before re-running it.",
142
148
  "accepted-job": "This section has a finished provider job that Slopify has not collected yet.",
143
149
  "control-changed": "Another action on this project (such as Pause or Cancel) happened at the same time. Reload the page to see its current state, then try again.",
@@ -148,7 +154,7 @@ export function actionRoutes(deps) {
148
154
  ...(result.intentRevisionId === undefined
149
155
  ? []
150
156
  : [
151
- "Your re-run request was saved. Use Resume to continue it instead of re-running again.",
157
+ "Your re-run request was saved. Use Continue the run to continue it instead of re-running again.",
152
158
  ]),
153
159
  ].join(" ");
154
160
  return problem(c, {
@@ -184,7 +190,7 @@ export function actionRoutes(deps) {
184
190
  ? parsed.data
185
191
  : controlled(c, id, { ok: false, reason: "revision-required" });
186
192
  };
187
- return new Hono()
193
+ return (new Hono()
188
194
  .post("/:id/pause", zValidator("param", idParam, onInvalid), async (c) => {
189
195
  const { id } = c.req.valid("param");
190
196
  const input = await controlInput(c, id);
@@ -208,19 +214,40 @@ export function actionRoutes(deps) {
208
214
  kind: "retry",
209
215
  stage: c.req.valid("param").kind,
210
216
  }))
217
+ // Soften and retry: the refused image prompts of this stage are reworded by the project's
218
+ // AI model when the retry draws them again (`slices/rebuild/soften.ts`).
219
+ .post("/:id/stages/:kind/soften", zValidator("param", stageParam, onInvalid), zValidator("json", revisionControlSchema, onInvalid), async (c) => {
220
+ const { id, kind } = c.req.valid("param");
221
+ const keys = kind === "images" || kind === "thumbnail" ? refusedKeys(deps.db, id, kind) : [];
222
+ if (keys.length === 0)
223
+ return problem(c, {
224
+ status: 409,
225
+ title: titleOf(409),
226
+ detail: "Nothing in this section was refused by a content filter, so there is no prompt to soften. Reload the page; if the section still shows an error, use Try again.",
227
+ extensions: { reason: "nothing-refused" },
228
+ });
229
+ requestSoftening(deps.db, id, keys, deps.clock.now().toISOString());
230
+ const response = await recovery(c, id, c.req.valid("json"), {
231
+ kind: "retry",
232
+ stage: kind,
233
+ });
234
+ if (response.status !== 202)
235
+ clearSoftening(deps.db, id, keys);
236
+ return response;
237
+ })
211
238
  .post("/:id/stages/:kind/rerun", zValidator("param", stageParam, onInvalid), zValidator("json", revisionControlSchema, onInvalid), (c) => recovery(c, c.req.valid("param").id, c.req.valid("json"), {
212
239
  kind: "rerun",
213
240
  stage: c.req.valid("param").kind,
214
241
  }))
215
242
  .put("/:id/article", zValidator("param", idParam, onInvalid), revisionRequired)
216
243
  .delete("/:id/images/:outputId", zValidator("param", imageParam, onInvalid), revisionRequired)
217
- .post("/:id/images/:outputId/regenerate", zValidator("param", imageParam, onInvalid), revisionRequired);
244
+ .post("/:id/images/:outputId/regenerate", zValidator("param", imageParam, onInvalid), revisionRequired));
218
245
  }
219
246
  function revisionRequired(c) {
220
247
  return problem(c, {
221
248
  status: 409,
222
249
  title: titleOf(409),
223
- detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Rebuild affected outputs.",
250
+ detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Choose what to remake (in the project's More menu).",
224
251
  extensions: { reason: "revision-required" },
225
252
  });
226
253
  }
@@ -2,9 +2,14 @@ import { existsSync } from "node:fs";
2
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
3
  import { Hono } from "hono";
4
4
  import { streamSSE } from "hono/streaming";
5
+ import { isSampleProject } from "../../slices/onboarding/sample.js";
5
6
  import { actionRoutes } from "./actions.js";
6
7
  import { audioPreviewRoutes } from "./audio-preview.js";
8
+ import { auditionRoutes } from "./auditions.js";
9
+ import { autostartRoutes } from "./autostart.js";
7
10
  import { backupRoutes } from "./backups.js";
11
+ import { channelMemoryRoutes } from "./channel-memory.js";
12
+ import { channelRoutes } from "./channels.js";
8
13
  import { checkpointRoutes } from "./checkpoints.js";
9
14
  import { diagnosticsRoutes } from "./diagnostics.js";
10
15
  import { documentThemeRoutes } from "./document-themes.js";
@@ -12,7 +17,11 @@ import { draftRoutes } from "./drafts.js";
12
17
  import { entryRoutes } from "./entries.js";
13
18
  import { fileRoutes } from "./files.js";
14
19
  import { fontsRoutes } from "./fonts.js";
20
+ import { homeRoutes, uploadedRoutes } from "./home.js";
21
+ import { narrationPeakRoutes } from "./narration-peaks.js";
22
+ import { onboardingRoutes } from "./onboarding.js";
15
23
  import { openFolderRoutes } from "./open-folder.js";
24
+ import { patchNotesRoutes } from "./patch-notes.js";
16
25
  import { planningRoutes } from "./planning.js";
17
26
  import { problem, problemFromError, titleOf } from "./problem.js";
18
27
  import { projectTemplateRoutes } from "./project-templates.js";
@@ -20,17 +29,26 @@ import { projectRoutes } from "./projects.js";
20
29
  import { promptRoutes } from "./prompts.js";
21
30
  import { pronunciationRoutes } from "./pronunciations.js";
22
31
  import { providerRoutes } from "./providers.js";
32
+ import { reviewRoutes } from "./reviews.js";
23
33
  import { revisionFileRoutes, revisionFolderRoutes } from "./revision-files.js";
24
34
  import { revisionRoutes } from "./revisions.js";
25
- import { scheduleRoutes } from "./schedules.js";
35
+ import { runCostRoutes } from "./run-cost.js";
36
+ import { calendarRoutes, scheduleRoutes } from "./schedules.js";
26
37
  import { settingsRoutes } from "./settings.js";
27
38
  import { stagingRoutes } from "./staging.js";
28
39
  import { storageRoutes } from "./storage.js";
40
+ import { filesRoutes } from "./storage-files.js";
41
+ import { studioRoutes } from "./studio.js";
42
+ import { stylePreviewRoutes } from "./style-preview.js";
29
43
  import { subtitleRoutes } from "./subtitles.js";
30
44
  import { telemetryRoutes } from "./telemetry.js";
45
+ import { trashRoutes } from "./trash.js";
31
46
  import { tutorialRoutes } from "./tutorial.js";
47
+ import { tutorialPagesRoutes } from "./tutorials.js";
32
48
  import { updateRoutes } from "./update.js";
33
49
  import { usageRoutes } from "./usage.js";
50
+ import { whatsNewRoutes } from "./whats-new.js";
51
+ import { youtubeEditRoutes } from "./youtube-edits.js";
34
52
  const notBuilt = "The Slopify UI is not built yet. The API is running: try /api/health.\n" +
35
53
  "Build the SPA with `npm run build` at the repository root.\n";
36
54
  function apiRoutes(deps, startedAt) {
@@ -41,34 +59,53 @@ function apiRoutes(deps, startedAt) {
41
59
  uptimeMs: deps.clock.now().getTime() - startedAt,
42
60
  }))
43
61
  .route("/staging", stagingRoutes(deps))
62
+ .route("/storage/files", filesRoutes(deps))
44
63
  .route("/storage", storageRoutes(deps))
45
64
  .route("/backups", backupRoutes(deps.backups))
65
+ .route("/trash", trashRoutes(deps))
46
66
  .route("/drafts", draftRoutes(deps.drafts))
47
67
  .route("/diagnostics", diagnosticsRoutes(deps))
48
68
  .route("/project-templates", projectTemplateRoutes(deps.drafts))
49
69
  .route("/schedules", scheduleRoutes(deps.schedules))
70
+ .route("/channels", channelRoutes(deps))
71
+ .route("/channels", channelMemoryRoutes(deps))
72
+ .route("/calendar", calendarRoutes(deps.schedules))
50
73
  .route("/projects", planningRoutes(deps))
51
74
  .route("/projects", projectRoutes(deps))
52
75
  .route("/projects", checkpointRoutes(deps))
76
+ .route("/projects", reviewRoutes(deps))
53
77
  .route("/projects", revisionRoutes(deps))
54
78
  .route("/projects", revisionFolderRoutes(deps))
55
79
  .route("/projects", openFolderRoutes(deps))
56
80
  .route("/projects", audioPreviewRoutes(deps))
81
+ .route("/projects", narrationPeakRoutes(deps))
82
+ .route("/projects", runCostRoutes(deps))
83
+ .route("/projects", uploadedRoutes(deps))
84
+ .route("/home", homeRoutes(deps))
57
85
  .route("/update", updateRoutes(deps))
58
86
  // The re-run and cancel actions sit on the same prefix as the project itself; they
59
87
  // are their own router because they are their own concern.
60
88
  .route("/projects", actionRoutes(deps))
61
89
  .route("/projects", subtitleRoutes(deps))
90
+ .route("/projects", youtubeEditRoutes(deps))
62
91
  .route("/fonts", fontsRoutes(deps))
63
92
  .route("/prompts", promptRoutes(deps))
64
93
  .route("/entries", entryRoutes(deps))
65
94
  .route("/document-themes", documentThemeRoutes(deps))
66
95
  .route("/pronunciations", pronunciationRoutes(deps))
96
+ .route("/auditions", auditionRoutes(deps))
67
97
  .route("/telemetry", telemetryRoutes(deps))
68
98
  .route("/usage", usageRoutes(deps))
99
+ .route("/settings/autostart", autostartRoutes(deps.autostart))
69
100
  .route("/settings", settingsRoutes(deps))
101
+ .route("/studio", studioRoutes(deps))
102
+ .route("/style-preview", stylePreviewRoutes(deps))
70
103
  .route("/tutorial", tutorialRoutes(deps))
71
- .route("/providers", providerRoutes(deps)));
104
+ .route("/whats-new", whatsNewRoutes(deps))
105
+ .route("/patch-notes", patchNotesRoutes(deps))
106
+ .route("/tutorials", tutorialPagesRoutes(deps))
107
+ .route("/providers", providerRoutes(deps))
108
+ .route("/onboarding", onboardingRoutes(deps)));
72
109
  }
73
110
  export function createApp(deps) {
74
111
  const startedAt = deps.clock.now().getTime();
@@ -117,6 +154,24 @@ export function createApp(deps) {
117
154
  releaseUpdate?.();
118
155
  releaseRequest?.();
119
156
  }
157
+ })
158
+ // The bundled sample is read-only, so exploring it never reaches a paid provider: only
159
+ // what reads or previews it is let through.
160
+ .use("/api/projects/:id/*", async (c, next) => {
161
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method))
162
+ return next();
163
+ const id = c.req.param("id");
164
+ // Deleting the sample is allowed: Settings → Restore sample brings it back.
165
+ if (!isSampleProject(deps.db, id) ||
166
+ sampleReadOnlyExempt.test(c.req.path) ||
167
+ c.req.path.replace(/\/$/, "") === `/api/projects/${id}`)
168
+ return next();
169
+ return problem(c, {
170
+ status: 409,
171
+ title: titleOf(409),
172
+ detail: "This is the sample project, which is read-only so trying things on it never spends anything. Press Make my own copy at the top of the sample's page, then edit and rebuild the copy.",
173
+ extensions: { reason: "sample-read-only" },
174
+ });
120
175
  })
121
176
  .onError((error, c) => problemFromError(c, error, deps))
122
177
  .notFound(missing)
@@ -141,6 +196,9 @@ export function createApp(deps) {
141
196
  }
142
197
  return app;
143
198
  }
199
+ // What the sample's page may still do: prepare its revision view, preview a rebuild (which
200
+ // starts nothing) and open its folder.
201
+ const sampleReadOnlyExempt = /\/(revisions\/prepare|rebuild\/preview|open-folder)\/?$/;
144
202
  function missing(c) {
145
203
  return problem(c, {
146
204
  status: 404,
@@ -0,0 +1,70 @@
1
+ import { zValidator } from "@hono/zod-validator";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ import { isProviderError } from "../../kernel/ports/model.js";
5
+ import { estimateRequests } from "../../slices/estimate/index.js";
6
+ import { auditionLineMax } from "../../slices/voices/audition.js";
7
+ import { onInvalid, problem } from "./problem.js";
8
+ const line = z.object({
9
+ provider: z.string().min(1).max(100),
10
+ model: z.string().min(1).max(200),
11
+ text: z.string().trim().min(1).max(auditionLineMax),
12
+ });
13
+ const quoteBody = z.object({
14
+ lines: z
15
+ .array(line.extend({ speaker: z.string().max(100) }))
16
+ .min(1)
17
+ .max(10),
18
+ });
19
+ const speakBody = line.extend({
20
+ voice: z.string().min(1).max(200),
21
+ // The page sends this only from the Audition button, after showing the price.
22
+ confirmed: z.literal(true),
23
+ });
24
+ export function auditionRoutes(deps) {
25
+ return new Hono()
26
+ .post("/quote", zValidator("json", quoteBody, onInvalid), (c) => {
27
+ const { lines } = c.req.valid("json");
28
+ if (deps.catalogue === undefined)
29
+ return c.json({ estimate: null });
30
+ const estimate = estimateRequests(lines.map((one) => ({
31
+ kind: "tts",
32
+ stage: one.speaker,
33
+ provider: one.provider,
34
+ model: one.model,
35
+ text: one.text,
36
+ })), deps.catalogue.read());
37
+ return c.json({ estimate });
38
+ })
39
+ .post("/", zValidator("json", speakBody, onInvalid), async (c) => {
40
+ const body = c.req.valid("json");
41
+ if (deps.audition === undefined)
42
+ return problem(c, {
43
+ status: 500,
44
+ title: "Internal Server Error",
45
+ detail: "Voice auditions aren't available in this build. Start the run and listen to the narration in the project instead.",
46
+ });
47
+ try {
48
+ const spoken = await deps.audition({ provider: body.provider, model: body.model, voiceId: body.voice, text: body.text }, c.req.raw.signal);
49
+ if (!spoken.ok)
50
+ return problem(c, {
51
+ status: 409,
52
+ title: "Conflict",
53
+ detail: "The audition was held back. Try the Audition button again in a moment.",
54
+ });
55
+ return c.body(spoken.value.bytes.slice().buffer, 200, {
56
+ "content-type": "audio/mpeg",
57
+ "cache-control": "no-store",
58
+ });
59
+ }
60
+ catch (error) {
61
+ if (isProviderError(error))
62
+ return problem(c, {
63
+ status: 502,
64
+ title: "Bad Gateway",
65
+ detail: `The audition couldn't be spoken: ${error.message}`,
66
+ });
67
+ throw error;
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,37 @@
1
+ import { zValidator } from "@hono/zod-validator";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ import { AutostartRefusal } from "../autostart/service.js";
5
+ import { onInvalid, problem, titleOf } from "./problem.js";
6
+ const body = z.object({ enabled: z.boolean() });
7
+ // Settings → General's "Start Slopify when I log in", and the first-run screen's offer of it.
8
+ // In Docker the view says whether Docker starts at login, and turning it on or off is refused
9
+ // with where Docker's own setting is.
10
+ export function autostartRoutes(service) {
11
+ const unavailable = "Starting at login can't be set from this Slopify. Start it with npx @gentbajko/slopify and try Settings → General again.";
12
+ return (new Hono()
13
+ .get("/", async (c) => {
14
+ if (service === undefined)
15
+ return problem(c, { status: 404, title: titleOf(404), detail: unavailable });
16
+ c.header("Cache-Control", "no-store");
17
+ return c.json(await service.view());
18
+ })
19
+ .put("/", zValidator("json", body, onInvalid), async (c) => {
20
+ if (service === undefined)
21
+ return problem(c, { status: 404, title: titleOf(404), detail: unavailable });
22
+ try {
23
+ return c.json(await service.set(c.req.valid("json").enabled));
24
+ }
25
+ catch (error) {
26
+ if (!(error instanceof AutostartRefusal))
27
+ throw error;
28
+ return problem(c, { status: 409, title: titleOf(409), detail: error.message });
29
+ }
30
+ })
31
+ // The first-run screen's No thanks: never offer it there again, change nothing.
32
+ .post("/answer", async (c) => {
33
+ if (service === undefined)
34
+ return problem(c, { status: 404, title: titleOf(404), detail: unavailable });
35
+ return c.json(await service.answer());
36
+ }));
37
+ }
@@ -0,0 +1,42 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { Readable } from "node:stream";
3
+ // One `bytes=` range, as browsers ask for media; anything else is answered with the whole file.
4
+ export function byteRange(header, size) {
5
+ const matched = /^bytes=(\d*)-(\d*)$/.exec(header?.trim() ?? "");
6
+ if (matched === null)
7
+ return undefined;
8
+ const [, from = "", to = ""] = matched;
9
+ if (from === "" && to === "")
10
+ return undefined;
11
+ if (from === "") {
12
+ const suffix = Number(to);
13
+ if (suffix === 0)
14
+ return "unsatisfiable";
15
+ return { start: Math.max(0, size - suffix), end: size - 1 };
16
+ }
17
+ const start = Number(from);
18
+ const end = to === "" ? size - 1 : Math.min(Number(to), size - 1);
19
+ if (start >= size || end < start)
20
+ return "unsatisfiable";
21
+ return { start, end };
22
+ }
23
+ // A file on disk as a response that honours one byte range, so an audio or video element
24
+ // can seek: it asks for the bytes at the new time and gets 206 with just that slice. Built
25
+ // with `c.body` so the app's own headers (the version, among others) are still added.
26
+ export function fileResponse(c, path, size, headers) {
27
+ const base = { ...headers, "accept-ranges": "bytes" };
28
+ const range = byteRange(c.req.header("range"), size);
29
+ if (range === "unsatisfiable")
30
+ return c.body(null, 416, { ...base, "content-range": `bytes */${String(size)}` });
31
+ if (size === 0)
32
+ return c.body(null, 200, { ...base, "content-length": "0" });
33
+ const { start, end } = range ?? { start: 0, end: size - 1 };
34
+ const body = Readable.toWeb(createReadStream(path, { start, end }));
35
+ return c.body(body, range === undefined ? 200 : 206, {
36
+ ...base,
37
+ "content-length": String(end - start + 1),
38
+ ...(range === undefined
39
+ ? {}
40
+ : { "content-range": `bytes ${String(start)}-${String(end)}/${String(size)}` }),
41
+ });
42
+ }
@@ -0,0 +1,92 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { zValidator } from "@hono/zod-validator";
3
+ import { Hono } from "hono";
4
+ import { bodyLimit } from "hono/body-limit";
5
+ import { z } from "zod";
6
+ import { clearChannelVideos, deleteChannelVideo, importChannelVideos, listChannelVideos, previewChannelVideos, } from "../../slices/channels/videos.js";
7
+ import { channelEpisodes, deleteMemory, editMemory, setEpisodeMemory, } from "../../slices/episodes/service.js";
8
+ import { onInvalid, problem, titleOf } from "./problem.js";
9
+ const idParam = z.object({ id: z.uuid() });
10
+ const memoryParam = idParam.extend({ memoryId: z.string().min(1).max(100) });
11
+ const videoParam = idParam.extend({ videoId: z.uuid() });
12
+ // A YouTube Studio export of a large channel is a few megabytes.
13
+ const importMaxBytes = 20 * 1024 * 1024;
14
+ // Channel page → Episodes (episode memory: the setting and the summaries) and Existing videos
15
+ // (titles made before Slopify; a CSV is previewed, then its ticked titles are saved). Mounted
16
+ // beside `channelRoutes` under /api/channels.
17
+ export function channelMemoryRoutes(deps) {
18
+ const service = { db: deps.db, clock: deps.clock, uuid: randomUUID };
19
+ return new Hono()
20
+ .get("/:id/episodes", zValidator("param", idParam, onInvalid), (c) => {
21
+ const result = channelEpisodes(service, c.req.valid("param").id);
22
+ return result.ok ? c.json(result.value) : refused(c, result, "episode");
23
+ })
24
+ .put("/:id/episodes/setting", zValidator("param", idParam, onInvalid), async (c) => {
25
+ const result = setEpisodeMemory(service, c.req.valid("param").id, await body(c));
26
+ return result.ok ? c.json(result.value) : refused(c, result, "episode");
27
+ })
28
+ .put("/:id/episodes/:memoryId", zValidator("param", memoryParam, onInvalid), async (c) => {
29
+ const { id, memoryId } = c.req.valid("param");
30
+ const result = editMemory(service, id, memoryId, await body(c));
31
+ return result.ok ? c.json(result.value) : refused(c, result, "episode");
32
+ })
33
+ .delete("/:id/episodes/:memoryId", zValidator("param", memoryParam, onInvalid), (c) => {
34
+ const { id, memoryId } = c.req.valid("param");
35
+ const result = deleteMemory(service, id, memoryId);
36
+ return result.ok ? c.body(null, 204) : refused(c, result, "episode");
37
+ })
38
+ .get("/:id/videos", zValidator("param", idParam, onInvalid), (c) => {
39
+ const result = listChannelVideos(service, c.req.valid("param").id);
40
+ return result.ok ? c.json({ videos: result.value }) : refused(c, result, "video");
41
+ })
42
+ .post("/:id/videos/preview", zValidator("param", idParam, onInvalid), importLimit(), async (c) => {
43
+ const result = previewChannelVideos(service, c.req.valid("param").id, await body(c));
44
+ return result.ok ? c.json(result.value) : refused(c, result, "video");
45
+ })
46
+ .post("/:id/videos", zValidator("param", idParam, onInvalid), importLimit(), async (c) => {
47
+ const result = importChannelVideos(service, c.req.valid("param").id, await body(c));
48
+ return result.ok ? c.json(result.value, 201) : refused(c, result, "video");
49
+ })
50
+ .delete("/:id/videos", zValidator("param", idParam, onInvalid), (c) => {
51
+ const result = clearChannelVideos(service, c.req.valid("param").id);
52
+ return result.ok ? c.json(result.value) : refused(c, result, "video");
53
+ })
54
+ .delete("/:id/videos/:videoId", zValidator("param", videoParam, onInvalid), (c) => {
55
+ const { id, videoId } = c.req.valid("param");
56
+ const result = deleteChannelVideo(service, id, videoId);
57
+ return result.ok ? c.body(null, 204) : refused(c, result, "video");
58
+ });
59
+ }
60
+ function importLimit() {
61
+ return bodyLimit({
62
+ maxSize: importMaxBytes,
63
+ onError: (c) => refused(c, {
64
+ ok: false,
65
+ reason: "invalid-input",
66
+ message: "This file is larger than 20 MB. Export fewer videos from YouTube Studio, or paste the titles instead.",
67
+ }, "video"),
68
+ });
69
+ }
70
+ async function body(c) {
71
+ try {
72
+ return await c.req.json();
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ function refused(c, result, about) {
79
+ const status = result.reason === "not-found" ? 404 : 400;
80
+ const detail = result.message ??
81
+ (result.reason === "not-found"
82
+ ? about === "episode"
83
+ ? "This channel or episode summary no longer exists; it may have been deleted in another tab. Reload the channel's Episodes tab."
84
+ : "This channel or video title no longer exists; it may have been deleted in another tab. Reload the channel's Existing videos tab."
85
+ : "This request isn't valid. Reload the page and try again.");
86
+ return problem(c, {
87
+ status,
88
+ title: titleOf(status),
89
+ detail,
90
+ extensions: { reason: result.reason },
91
+ });
92
+ }