@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,531 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream, lstatSync, readdirSync } from "node:fs";
3
+ import { access, constants, copyFile, lstat, mkdir, readdir, rename, statfs, utimes, } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
5
+ import { z } from "zod";
6
+ import { repoint } from "../../kernel/paths.js";
7
+ import { readSetting, writeSetting } from "../settings/repo.js";
8
+ import { backupsFolderName } from "./layout.js";
9
+ // Where the files a user looks at live: project folders, scheduled backups and anything else
10
+ // Slopify saves for them. New installs keep them in <Documents>/Slopify; installs from before
11
+ // 3.0 keep them where they were, inside the data dir, until the user moves them from Settings →
12
+ // Backup & storage. The database, settings, models and logs always stay in the data dir.
13
+ export const filesLocationKey = "files.location";
14
+ // A move that was started and not finished: its target, so the next try picks up the files it
15
+ // already copied instead of refusing a folder that isn't empty any more.
16
+ export const filesMoveKey = "files.move";
17
+ export const filesRootName = "Slopify";
18
+ const locationSchema = z.discriminatedUnion("kind", [
19
+ // Everything inside the data dir, the way every install before 3.0 kept it. Stored as a kind,
20
+ // not a path, so a data dir moved by hand still finds its projects.
21
+ z.object({ kind: z.literal("data-dir") }).strict(),
22
+ z.object({ kind: z.literal("root"), root: z.string().min(1) }).strict(),
23
+ ]);
24
+ const moveSchema = z.object({ target: z.string().min(1), from: locationSchema }).strict();
25
+ export function filesLayoutOf(location, dataDir) {
26
+ if (location.kind === "root")
27
+ return {
28
+ projects: join(location.root, "Projects"),
29
+ backups: join(location.root, backupsFolderName),
30
+ exports: join(location.root, "Exports"),
31
+ };
32
+ const projects = join(resolve(dataDir), "projects");
33
+ return { projects, backups: join(projects, backupsFolderName), exports: null };
34
+ }
35
+ export function readFilesLocation(db) {
36
+ const stored = readSetting(db, filesLocationKey);
37
+ if (stored === undefined)
38
+ return undefined;
39
+ try {
40
+ return locationSchema.parse(JSON.parse(stored));
41
+ }
42
+ catch {
43
+ throw new Error(`Slopify's saved files folder setting is damaged, so it doesn't know where your projects are. Start Slopify once with its files in the data dir by deleting the "${filesLocationKey}" row from the settings table, or restore the database from a backup.`);
44
+ }
45
+ }
46
+ // Decides, once per install, where its files live, and remembers it. An install that already
47
+ // had a database keeps its files where they are; a new one gets <Documents>/Slopify, or
48
+ // "Slopify 2", "Slopify 3"… when that one already holds another install's projects, because
49
+ // storage cleanup deletes project folders its own database doesn't know.
50
+ export async function settleFilesLocation(input) {
51
+ const stored = readFilesLocation(input.db);
52
+ if (stored !== undefined)
53
+ return stored;
54
+ let location = { kind: "data-dir" };
55
+ if (input.fresh && input.documents !== undefined) {
56
+ const documents = await input.documents();
57
+ for (let n = 1; n <= 20; n++) {
58
+ const root = join(documents, n === 1 ? filesRootName : `${filesRootName} ${n}`);
59
+ if (await emptyOrMissing(join(root, "Projects"))) {
60
+ location = { kind: "root", root };
61
+ break;
62
+ }
63
+ }
64
+ }
65
+ writeSetting(input.db, filesLocationKey, JSON.stringify(location));
66
+ return location;
67
+ }
68
+ // Creates the visible folders of a root, so the user finds Projects and Backups the first time
69
+ // they look. Exports is only made when something is saved there.
70
+ export async function ensureFilesFolders(layout) {
71
+ await mkdir(layout.projects, { recursive: true });
72
+ await mkdir(layout.backups, { recursive: true });
73
+ }
74
+ async function emptyOrMissing(path) {
75
+ const entries = await readdir(path).catch((error) => {
76
+ if (hasCode(error, "ENOENT"))
77
+ return [];
78
+ throw error;
79
+ });
80
+ return entries.length === 0;
81
+ }
82
+ export const dockerMoveCommand = "npx @gentbajko/slopify@latest update --docker --projects-dir documents";
83
+ export function createFilesService(deps) {
84
+ let job;
85
+ let finished;
86
+ // From the first check to the start of the copy, so two presses never start two moves.
87
+ let starting = false;
88
+ const location = () => readFilesLocation(deps.db) ?? { kind: "data-dir" };
89
+ const pending = () => {
90
+ const stored = readSetting(deps.db, filesMoveKey);
91
+ if (stored === undefined)
92
+ return undefined;
93
+ try {
94
+ return moveSchema.parse(JSON.parse(stored));
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ };
100
+ const documentsRoot = async () => join(resolve(await deps.documents()), filesRootName);
101
+ // Checks the target and starts the copy in the background; the view reports its progress.
102
+ const begin = async (requested) => {
103
+ let target;
104
+ try {
105
+ target = requested === "documents" ? await documentsRoot() : requested.trim();
106
+ }
107
+ catch {
108
+ return {
109
+ ok: false,
110
+ status: 409,
111
+ detail: "Slopify couldn't find your Documents folder. Use Choose another folder in Settings → Backup & storage and type the full path of the folder you want instead.",
112
+ };
113
+ }
114
+ const from = location();
115
+ const stored = pending();
116
+ const resuming = stored !== undefined && samePath(stored.target, target);
117
+ const problem = await targetProblem(deps, from, target, resuming);
118
+ if (problem !== undefined)
119
+ return { ok: false, status: 400, detail: problem };
120
+ target = resolve(target);
121
+ const busy = deps.busy();
122
+ if (busy.length > 0)
123
+ return { ok: false, status: 409, detail: busyDetail(busy) };
124
+ const release = deps.beginMutation === undefined ? () => { } : deps.beginMutation();
125
+ if (release === undefined)
126
+ return {
127
+ ok: false,
128
+ status: 409,
129
+ detail: "Slopify is installing an update right now. Wait for it to restart, then press the button again in Settings → Backup & storage.",
130
+ };
131
+ const pairs = sourcePairs(deps.paths, from, target);
132
+ const free = deps.freeBytes ?? freeBytesAt;
133
+ let needed;
134
+ try {
135
+ needed = pairs.reduce((sum, pair) => sum + pendingBytes(pair), 0);
136
+ }
137
+ catch (error) {
138
+ release();
139
+ throw error;
140
+ }
141
+ const available = await free(nearestExisting(target)).catch(() => Number.POSITIVE_INFINITY);
142
+ if (available < needed + 64 * 1024 * 1024) {
143
+ release();
144
+ return {
145
+ ok: false,
146
+ status: 400,
147
+ detail: `There isn't enough free space for your files in ${target}: they need ${formatBytes(needed)} and the disk has ${formatBytes(available)} free. Free up space there, or choose a folder on another disk with Choose another folder.`,
148
+ };
149
+ }
150
+ writeSetting(deps.db, filesMoveKey, JSON.stringify({ target, from }));
151
+ finished = undefined;
152
+ const progress = {
153
+ target,
154
+ phase: "copying",
155
+ files: 0,
156
+ totalFiles: 0,
157
+ bytes: 0,
158
+ totalBytes: 0,
159
+ error: null,
160
+ oldFolder: null,
161
+ };
162
+ const current = { progress, done: Promise.resolve() };
163
+ job = current;
164
+ const update = (change) => {
165
+ current.progress = { ...current.progress, ...change };
166
+ };
167
+ current.done = runMove(deps, from, target, pairs, update)
168
+ .then((oldFolder) => {
169
+ finished = { ...current.progress, phase: "done", oldFolder };
170
+ })
171
+ .catch((error) => {
172
+ finished = {
173
+ ...current.progress,
174
+ phase: "failed",
175
+ error: error instanceof Error ? error.message : String(error),
176
+ };
177
+ })
178
+ .finally(() => {
179
+ job = undefined;
180
+ release();
181
+ });
182
+ return { ok: true };
183
+ };
184
+ return {
185
+ view: async () => {
186
+ if (deps.docker !== undefined)
187
+ return {
188
+ docker: true,
189
+ folder: deps.docker.hostProjects,
190
+ projects: deps.docker.hostProjects,
191
+ backups: deps.docker.hostBackups,
192
+ exports: null,
193
+ inDataDir: false,
194
+ documentsRoot: null,
195
+ inDocuments: false,
196
+ move: null,
197
+ dockerCommand: dockerMoveCommand,
198
+ };
199
+ const here = location();
200
+ const root = await documentsRoot().catch(() => null);
201
+ const stored = pending();
202
+ const move = job?.progress ??
203
+ finished ??
204
+ (stored === undefined
205
+ ? null
206
+ : {
207
+ target: stored.target,
208
+ phase: "interrupted",
209
+ files: 0,
210
+ totalFiles: 0,
211
+ bytes: 0,
212
+ totalBytes: 0,
213
+ error: null,
214
+ oldFolder: null,
215
+ });
216
+ return {
217
+ docker: false,
218
+ folder: here.kind === "root" ? here.root : deps.paths.projects,
219
+ projects: deps.paths.projects,
220
+ backups: deps.paths.backups,
221
+ exports: deps.paths.exports,
222
+ inDataDir: here.kind === "data-dir",
223
+ documentsRoot: root,
224
+ inDocuments: here.kind === "root" && root !== null && samePath(here.root, root),
225
+ move,
226
+ dockerCommand: null,
227
+ };
228
+ },
229
+ move: async (requested) => {
230
+ if (deps.docker !== undefined)
231
+ return {
232
+ ok: false,
233
+ status: 409,
234
+ detail: `Slopify runs in Docker here, and a container can't move the folder it was started with. On the computer running Docker, run ${dockerMoveCommand} (or --projects-dir <folder> for another folder); it waits for running work, copies your projects and checks the copy.`,
235
+ };
236
+ if (job !== undefined || starting)
237
+ return {
238
+ ok: false,
239
+ status: 409,
240
+ detail: `Your files are already being moved${job === undefined ? "" : ` to ${job.progress.target}`}. Wait for it to finish; Settings → Backup & storage shows its progress.`,
241
+ };
242
+ starting = true;
243
+ try {
244
+ return await begin(requested);
245
+ }
246
+ finally {
247
+ starting = false;
248
+ }
249
+ },
250
+ open: async () => {
251
+ if (deps.docker !== undefined) {
252
+ const host = deps.docker.hostProjects;
253
+ if (host === null)
254
+ throw new Error("No host folder");
255
+ const opened = (await deps.docker.openOnHost?.(host, AbortSignal.timeout(10_000)).catch(() => false)) ===
256
+ true;
257
+ return { opened, path: host };
258
+ }
259
+ const here = location();
260
+ const folder = here.kind === "root" ? here.root : deps.paths.projects;
261
+ if (deps.openFolder === undefined)
262
+ return { opened: false, path: folder };
263
+ await deps.openFolder(folder);
264
+ return { opened: true, path: folder };
265
+ },
266
+ idle: async () => {
267
+ await job?.done;
268
+ },
269
+ };
270
+ }
271
+ // What moves where. An install that keeps its files in the data dir has Backups inside its
272
+ // projects folder; the new root has it beside Projects.
273
+ function sourcePairs(paths, from, target) {
274
+ const next = filesLayoutOf({ kind: "root", root: target }, "");
275
+ const pairs = [
276
+ {
277
+ from: paths.projects,
278
+ to: next.projects,
279
+ skip: from.kind === "data-dir" ? backupsFolderName : null,
280
+ },
281
+ { from: paths.backups, to: next.backups, skip: null },
282
+ ];
283
+ if (paths.exports !== null && next.exports !== null)
284
+ pairs.push({ from: paths.exports, to: next.exports, skip: null });
285
+ return pairs;
286
+ }
287
+ // Every regular file under a pair's source, relative to it. Links are left out: Slopify never
288
+ // makes one, and following one could copy a folder the user never meant to move.
289
+ function sourceFiles(pair) {
290
+ const files = [];
291
+ const walk = (dir) => {
292
+ let entries;
293
+ try {
294
+ entries = readdirSync(dir, { withFileTypes: true });
295
+ }
296
+ catch (error) {
297
+ if (hasCode(error, "ENOENT"))
298
+ return;
299
+ throw error;
300
+ }
301
+ for (const entry of entries) {
302
+ const full = join(dir, entry.name);
303
+ if (dir === pair.from && entry.name === pair.skip)
304
+ continue;
305
+ if (entry.isDirectory())
306
+ walk(full);
307
+ else if (entry.isFile()) {
308
+ const stat = lstatSync(full);
309
+ files.push({ path: relative(pair.from, full), size: stat.size, mtimeMs: stat.mtimeMs });
310
+ }
311
+ }
312
+ };
313
+ walk(pair.from);
314
+ return files;
315
+ }
316
+ function pendingBytes(pair) {
317
+ let bytes = 0;
318
+ for (const file of sourceFiles(pair)) {
319
+ const there = lstatSync(join(pair.to, file.path), { throwIfNoEntry: false });
320
+ if (!(there?.isFile() && there.size === file.size && there.mtimeMs === file.mtimeMs))
321
+ bytes += file.size;
322
+ }
323
+ return bytes;
324
+ }
325
+ // Copy, check every file's size and SHA-256 against the original, then switch. A file already
326
+ // in the target with the same size and time is one an interrupted move copied, and is only
327
+ // checked. The old folder is never changed.
328
+ async function runMove(deps, from, target, pairs, update) {
329
+ const listed = pairs.map((pair) => ({ pair, files: sourceFiles(pair) }));
330
+ const totalFiles = listed.reduce((sum, item) => sum + item.files.length, 0);
331
+ const totalBytes = listed.reduce((sum, item) => sum + item.files.reduce((s, f) => s + f.size, 0), 0);
332
+ update({ totalFiles, totalBytes });
333
+ const copyOne = async (pair, file) => {
334
+ const source = join(pair.from, file.path);
335
+ const destination = join(pair.to, file.path);
336
+ const partial = `${destination}.slopify-partial`;
337
+ await mkdir(dirname(destination), { recursive: true });
338
+ await copyFile(source, partial);
339
+ await utimes(partial, new Date(), new Date(file.mtimeMs));
340
+ await rename(partial, destination);
341
+ };
342
+ for (const pair of pairs)
343
+ await mkdir(pair.to, { recursive: true });
344
+ let files = 0;
345
+ let bytes = 0;
346
+ for (const { pair, files: list } of listed)
347
+ for (const file of list) {
348
+ const there = await lstat(join(pair.to, file.path)).catch(() => undefined);
349
+ if (!(there?.isFile() && there.size === file.size && there.mtimeMs === file.mtimeMs))
350
+ await copyOne(pair, file);
351
+ files += 1;
352
+ bytes += file.size;
353
+ update({ files, bytes });
354
+ }
355
+ update({ phase: "verifying", files: 0, bytes: 0 });
356
+ files = 0;
357
+ bytes = 0;
358
+ for (const { pair, files: list } of listed)
359
+ for (const file of list) {
360
+ const source = join(pair.from, file.path);
361
+ const destination = join(pair.to, file.path);
362
+ if (!(await sameContent(source, destination))) {
363
+ await copyOne(pair, file);
364
+ if (!(await sameContent(source, destination)))
365
+ throw new Error(`The copy of ${source} in ${destination} doesn't match the original, so Slopify kept using the old folder. Check the disk holding ${target} has no errors, then press Continue moving in Settings → Backup & storage.`);
366
+ }
367
+ files += 1;
368
+ bytes += file.size;
369
+ update({ files, bytes });
370
+ }
371
+ // The last look and the switch happen in one go, so nothing can start writing in between.
372
+ const busy = deps.busy();
373
+ if (busy.length > 0)
374
+ throw new Error(`A project started while your files were being copied, so Slopify kept using the old folder. Everything copied so far is kept: once ${busy[0]?.title === undefined ? "it" : `"${busy[0].title}"`} has finished, press Continue moving in Settings → Backup & storage.`);
375
+ for (const { pair, files: list } of listed) {
376
+ const now = sourceFiles(pair);
377
+ const before = new Map(list.map((file) => [file.path, file]));
378
+ for (const file of now) {
379
+ const was = before.get(file.path);
380
+ if (was === undefined || was.size !== file.size || was.mtimeMs !== file.mtimeMs)
381
+ throw new Error(`Files in ${pair.from} changed while they were being copied (${file.path}), so Slopify kept using the old folder. Press Continue moving in Settings → Backup & storage; it copies only what changed.`);
382
+ }
383
+ }
384
+ const to = { kind: "root", root: target };
385
+ writeSetting(deps.db, filesLocationKey, JSON.stringify(to));
386
+ deps.db.prepare("DELETE FROM settings WHERE key=?").run(filesMoveKey);
387
+ const old = from.kind === "root" ? from.root : deps.paths.projects;
388
+ repoint(deps.paths, filesLayoutOf(to, deps.paths.dataDir));
389
+ return old;
390
+ }
391
+ async function sameContent(a, b) {
392
+ const [sa, sb] = await Promise.all([lstat(a), lstat(b).catch(() => undefined)]);
393
+ if (sb === undefined || !sb.isFile() || sa.size !== sb.size)
394
+ return false;
395
+ const [ha, hb] = await Promise.all([sha256(a), sha256(b)]);
396
+ return ha === hb;
397
+ }
398
+ async function sha256(path) {
399
+ const hash = createHash("sha256");
400
+ for await (const chunk of createReadStream(path))
401
+ hash.update(chunk);
402
+ return hash.digest("hex");
403
+ }
404
+ // ----- choosing a folder ------------------------------------------------------------------
405
+ const unixSystem = [
406
+ "/bin",
407
+ "/boot",
408
+ "/dev",
409
+ "/etc",
410
+ "/lib",
411
+ "/lib32",
412
+ "/lib64",
413
+ "/opt",
414
+ "/proc",
415
+ "/root",
416
+ "/run",
417
+ "/sbin",
418
+ "/snap",
419
+ "/srv",
420
+ "/sys",
421
+ "/usr",
422
+ "/var",
423
+ "/System",
424
+ "/Library",
425
+ "/Applications",
426
+ "/private",
427
+ "/cores",
428
+ ];
429
+ const unixShared = ["/", "/home", "/Users", "/mnt", "/media", "/Volumes", "/tmp"];
430
+ // Why a folder can't hold Slopify's files, as the sentence Choose another folder shows;
431
+ // undefined when it can.
432
+ export async function targetProblem(deps, from, requested, resuming) {
433
+ const platform = deps.platform ?? process.platform;
434
+ if (requested === "" || !isAbsolute(requested))
435
+ return `Enter the folder's full path, starting from the top of the disk (for example ${platform === "win32" ? "D:\\Slopify" : "/home/you/Videos/Slopify"}).`;
436
+ if (/[\p{Cc}]/u.test(requested))
437
+ return "The folder's name has a line break or another control character in it. Choose a folder whose name has none.";
438
+ const target = resolve(requested);
439
+ if (systemFolder(target, platform, deps.home))
440
+ return `${target} is a system or shared folder. Choose a folder of your own, for example one inside your Documents or Videos folder.`;
441
+ if (within(deps.paths.dataDir, target))
442
+ return `${target} is inside Slopify's data folder (${deps.paths.dataDir}), which holds its database and settings. Choose a folder outside it.`;
443
+ if (from.kind === "root" && samePath(from.root, target))
444
+ return `Your files are already in ${target}.`;
445
+ const next = filesLayoutOf({ kind: "root", root: target }, "");
446
+ for (const [mine, theirs] of [
447
+ [deps.paths.projects, next.projects],
448
+ [deps.paths.projects, next.backups],
449
+ [deps.paths.backups, next.projects],
450
+ [deps.paths.backups, next.backups],
451
+ [deps.paths.projects, target],
452
+ ])
453
+ if (within(mine, theirs) || (theirs !== target && within(theirs, mine)))
454
+ return `${target} overlaps the folder your files are in now (${mine}), so copying would copy the folder into itself. Choose a separate folder.`;
455
+ if (!resuming)
456
+ for (const folder of [next.projects, next.backups])
457
+ if (!(await emptyOrMissing(folder).catch(() => false)))
458
+ return `${folder} already has files in it. Slopify cleans up its Projects folder on its own, so it only moves into an empty one: choose another folder, or move those files away first.`;
459
+ const existing = nearestExisting(target);
460
+ const stat = await lstat(existing).catch(() => undefined);
461
+ if (stat === undefined || !stat.isDirectory())
462
+ return `${existing} is a file, not a folder, so ${target} can't be made there. Choose another folder.`;
463
+ try {
464
+ await access(existing, constants.W_OK);
465
+ }
466
+ catch {
467
+ return `Slopify isn't allowed to write in ${existing}. Choose a folder your user can write to, or change that folder's permissions.`;
468
+ }
469
+ return undefined;
470
+ }
471
+ function systemFolder(path, platform, home) {
472
+ if (platform === "win32") {
473
+ const p = path.replace(/[\\/]+$/, "").toLowerCase();
474
+ if (parse(path)
475
+ .root.replace(/[\\/]+$/, "")
476
+ .toLowerCase() === p)
477
+ return true;
478
+ const env = process.env;
479
+ return [env.SystemRoot, env.ProgramFiles, env["ProgramFiles(x86)"], env.ProgramData]
480
+ .filter((dir) => dir !== undefined && dir !== "")
481
+ .some((dir) => within(dir, path));
482
+ }
483
+ if (unixShared.includes(path) || (home !== undefined && samePath(home, path)))
484
+ return true;
485
+ return unixSystem.some((dir) => within(dir, path));
486
+ }
487
+ function nearestExisting(path) {
488
+ let current = resolve(path);
489
+ for (;;) {
490
+ if (lstatSync(current, { throwIfNoEntry: false }) !== undefined)
491
+ return current;
492
+ const parent = dirname(current);
493
+ if (parent === current)
494
+ return current;
495
+ current = parent;
496
+ }
497
+ }
498
+ async function freeBytesAt(path) {
499
+ const stats = await statfs(path);
500
+ return Number(stats.bavail) * Number(stats.bsize);
501
+ }
502
+ function busyDetail(projects) {
503
+ const named = projects
504
+ .slice(0, 3)
505
+ .map((project) => `"${project.title}"`)
506
+ .join(", ");
507
+ const more = projects.length > 3 ? ` and ${projects.length - 3} more` : "";
508
+ return `Slopify can't move your files while projects are being made (${named}${more}): their files are still being written. Wait for them to finish, or pause them on their project page, then press the button again.`;
509
+ }
510
+ function within(root, path) {
511
+ const r = relative(root, path);
512
+ return r === "" || (r !== ".." && !r.startsWith(`..${sep}`) && !isAbsolute(r));
513
+ }
514
+ function samePath(a, b) {
515
+ return relative(resolve(a), resolve(b)) === "";
516
+ }
517
+ function hasCode(error, code) {
518
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
519
+ }
520
+ function formatBytes(bytes) {
521
+ if (!Number.isFinite(bytes))
522
+ return "unknown";
523
+ const units = ["B", "KB", "MB", "GB", "TB"];
524
+ let value = bytes;
525
+ let unit = 0;
526
+ while (value >= 1024 && unit < units.length - 1) {
527
+ value /= 1024;
528
+ unit += 1;
529
+ }
530
+ return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
531
+ }
@@ -5,13 +5,13 @@ export function projectDir(paths, projectId) {
5
5
  export function outputPath(paths, projectId, relativePath) {
6
6
  return contained(projectDir(paths, projectId), relativePath);
7
7
  }
8
- // Scheduled backups (slices/backups) land here unless the user picks another folder. It sits
9
- // inside the projects root because that is the one folder a Docker install shares with the
10
- // host, so the archives end up on the user's disk and not only in the container's volume.
8
+ // Scheduled backups (slices/backups) land in paths.backups unless the user picks another
9
+ // folder. Installs from before 3.0 keep it inside the projects root, since that is the one
10
+ // folder an older Docker install shares with the host; newer ones have it beside Projects.
11
11
  // Project folders are ULIDs, so the name never collides with one; reconcile leaves it alone.
12
12
  export const backupsFolderName = "Backups";
13
13
  export function defaultBackupsDir(paths) {
14
- return contained(paths.projects, backupsFolderName);
14
+ return paths.backups;
15
15
  }
16
16
  export function stagingPath(paths, stagedFileId) {
17
17
  return contained(paths.staging, stagedFileId);
@@ -76,6 +76,8 @@ export function outputFileName(role, index, extension, stageKind) {
76
76
  return `shorts/short-${String(index).padStart(2, "0")}${extension}`;
77
77
  case "animated_image":
78
78
  return `animated/image-${String(index).padStart(3, "0")}${extension}`;
79
+ case "figure_card":
80
+ return `cards/card-${String(index).padStart(3, "0")}${extension}`;
79
81
  case "audio_body":
80
82
  return `audio-body${extension}`;
81
83
  case "audio_export":
@@ -86,6 +88,14 @@ export function outputFileName(role, index, extension, stageKind) {
86
88
  return `audio-outro${extension}`;
87
89
  case "thumbnail":
88
90
  return `thumbnail${extension}`;
91
+ case "script_md":
92
+ return "script.md";
93
+ case "audio_mp3":
94
+ return "narration.mp3";
95
+ case "audio_m4b":
96
+ return "audiobook.m4b";
97
+ case "audio_levelled":
98
+ return `audio-levelled${extension}`;
89
99
  case "reference":
90
100
  return `reference${extension}`;
91
101
  case "image":
@@ -33,8 +33,19 @@ export const outputRoles = [
33
33
  "short_video",
34
34
  // An image of the slideshow brought to life by an image-to-video model (Animate images).
35
35
  "animated_image",
36
+ // "Show tables and figures on screen": a table, figure, equation or code block of the
37
+ // article drawn as a card, in the video's format (and upright for Shorts).
38
+ "figure_card",
36
39
  // The Images stage's establishing image: made or uploaded first, drawn from by the other
37
40
  // images, never shown in the video.
38
41
  "reference",
42
+ // Multiple voices: the speaker split of an audiobook's text, and the listening files with
43
+ // chapter markers made beside the video.
44
+ "script_md",
45
+ "audio_mp3",
46
+ "audio_m4b",
47
+ // Level the volume: a narration segment joined again from its pieces, each brought to one
48
+ // loudness first (`meta.segment` says which). The exports play it in place of the plain join.
49
+ "audio_levelled",
39
50
  ];
40
51
  export const stagedFileStates = ["copying", "staged"];