@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,12 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statfsSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statfsSync, } from "node:fs";
3
3
  import { open } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
6
  import { ZodError } from "zod";
7
7
  import { migrate, newestMigration } from "../../kernel/db/migrate.js";
8
8
  import { transact } from "../../kernel/db/tx.js";
9
- import { projectById } from "../admission/repo.js";
9
+ import { projectByIdIncludingTrash } from "../admission/repo.js";
10
+ import { castOfChannel, channelById } from "../channels/repo.js";
10
11
  import { documentThemeNameMax, documentThemeNameProblems } from "../document/model.js";
11
12
  import { documentThemeSchema } from "../document/theme-schema.js";
12
13
  import { nameMax } from "../library/model.js";
@@ -16,7 +17,7 @@ import { revisionById } from "../revisions/repo.js";
16
17
  import { runsForSchedule, scheduleRows } from "../schedules/repo.js";
17
18
  import { allTelemetryEvents } from "../telemetry/repo.js";
18
19
  import { projectRows, rowsOf } from "./backup-export.js";
19
- import { backupMaxJsonBytes, backupMaxManifestBytes, backupSchemaVersion, checksumsMember, checksumsSchema, fontMember, libraryMember, libraryPartSchema, libraryTables, manifestHead, manifestMember, manifestSchema, projectFileMember, projectMember, projectPartSchema, projectTables, stagedMember, usageMember, usagePartSchema, usageTables, } from "./backup-format.js";
20
+ import { backupMaxJsonBytes, backupMaxManifestBytes, backupSchemaVersion, checksumsMember, checksumsSchema, fontMember, imageMember, libraryMember, libraryPartSchema, libraryTables, manifestHead, manifestMember, manifestSchema, projectFileMember, projectMember, projectPartSchema, projectTables, stagedMember, usageMember, usagePartSchema, usageTables, } from "./backup-format.js";
20
21
  import { outputPath, projectDir, stagingPath } from "./layout.js";
21
22
  import { checkLibraryRows, exportableSettings, installedUploadedFontFiles, validFontContent, } from "./portable.js";
22
23
  import { readTar, TarFormatError } from "./tar.js";
@@ -225,7 +226,10 @@ async function receive(body, file) {
225
226
  finally {
226
227
  await handle.close();
227
228
  }
228
- return hash.digest("hex");
229
+ const digest = hash.digest("hex");
230
+ if (file.image !== undefined && digest !== file.image)
231
+ throw damaged(`the picture ${file.image} in it is damaged.`);
232
+ return digest;
229
233
  }
230
234
  // Loads every row into a scratch database at the schema the backup was written with, carries
231
235
  // it forward with this build's migrations, and checks it with the same readers the app uses.
@@ -282,6 +286,16 @@ function prepare(deps, work, manifest, library, usage, parts, expected) {
282
286
  font: { id, extension },
283
287
  });
284
288
  }
289
+ for (const image of library.images ?? []) {
290
+ const keep = deps.db.prepare("SELECT 1 FROM image_blobs WHERE sha256=?").get(image.sha256) === undefined;
291
+ expect(imageMember(image.sha256), {
292
+ bytes: image.bytes,
293
+ destination: keep ? join(work, "images", image.sha256) : undefined,
294
+ image: image.sha256,
295
+ });
296
+ if (keep)
297
+ needed += image.bytes;
298
+ }
285
299
  for (const staged of library.staged) {
286
300
  const keep = keptStaged.has(staged.id);
287
301
  expect(stagedMember(staged.id), {
@@ -292,7 +306,7 @@ function prepare(deps, work, manifest, library, usage, parts, expected) {
292
306
  needed += staged.bytes;
293
307
  }
294
308
  checkFreeSpace(deps.paths.dataDir, needed);
295
- return { scratch, projects, skippedProjects, drafts };
309
+ return { scratch, projects, skippedProjects, drafts, images: library.images ?? [] };
296
310
  }
297
311
  catch (error) {
298
312
  scratch.close();
@@ -319,12 +333,23 @@ function loadScratch(clock, manifest, library, usage, parts) {
319
333
  if (known.size === 0 || Object.keys(row).some((column) => !known.has(column)))
320
334
  throw damaged(`it has ${table} records that don't fit the Slopify version it names.`);
321
335
  try {
322
- insertRow(scratch, table, row);
336
+ // The default channel is already there: every install makes it (0032) with the same id.
337
+ insertRow(scratch, table, row, table === "channels" ? "INSERT OR REPLACE" : "INSERT");
323
338
  }
324
339
  catch {
325
340
  throw damaged(`it has a ${table} record Slopify can't read.`);
326
341
  }
327
342
  };
343
+ // The pictures' bytes arrive after this, as files; a stand-in row lets their links check.
344
+ for (const image of library.images ?? [])
345
+ try {
346
+ scratch
347
+ .prepare("INSERT OR IGNORE INTO image_blobs(sha256,mime,bytes,created_at) VALUES (?,?,X'00','')")
348
+ .run(image.sha256, image.mime);
349
+ }
350
+ catch {
351
+ throw damaged("it has pictures that don't fit the Slopify version it names.");
352
+ }
328
353
  for (const table of libraryTables)
329
354
  for (const row of library.tables[table] ?? [])
330
355
  insert(table, row);
@@ -370,11 +395,20 @@ function loadScratch(clock, manifest, library, usage, parts) {
370
395
  function checkScratch(deps, scratch, library, parts) {
371
396
  const unreadable = (what) => damaged(`it has ${what} Slopify can't read.`);
372
397
  try {
398
+ // Names are unique among the items outside the trash (0039); one in the trash is checked
399
+ // on its own.
400
+ const prompt = "SELECT id,kind,name,body,slots,updated_at FROM prompts";
401
+ const entry = "SELECT id,category,mode,name,body,slots,updated_at FROM entries";
373
402
  checkLibraryRows({
374
- prompts: rowsOf(scratch, "SELECT * FROM prompts"),
375
- entries: rowsOf(scratch, "SELECT * FROM entries"),
376
- voices: rowsOf(scratch, "SELECT * FROM voices"),
403
+ prompts: rowsOf(scratch, `${prompt} WHERE deleted_at IS NULL`),
404
+ entries: rowsOf(scratch, `${entry} WHERE deleted_at IS NULL`),
405
+ // A voice's languages are checked as the settings export reads them (`languages`).
406
+ voices: rowsOf(scratch, "SELECT id,provider,name,voice_id,languages_json FROM voices").map(({ languages_json: languages, ...voice }) => typeof languages === "string" ? { ...voice, languages: JSON.parse(languages) } : voice),
377
407
  });
408
+ for (const row of rowsOf(scratch, `${prompt} WHERE deleted_at IS NOT NULL`))
409
+ checkLibraryRows({ prompts: [row], entries: [], voices: [] });
410
+ for (const row of rowsOf(scratch, `${entry} WHERE deleted_at IS NOT NULL`))
411
+ checkLibraryRows({ prompts: [], entries: [row], voices: [] });
378
412
  }
379
413
  catch {
380
414
  throw unreadable("a prompt, intro, outro or voice");
@@ -401,6 +435,16 @@ function checkScratch(deps, scratch, library, parts) {
401
435
  catch {
402
436
  throw unreadable("a document theme");
403
437
  }
438
+ try {
439
+ for (const row of rowsOf(scratch, "SELECT id FROM channels")) {
440
+ if (channelById(scratch, String(row.id)) === undefined)
441
+ throw new Error("channel");
442
+ castOfChannel(scratch, String(row.id));
443
+ }
444
+ }
445
+ catch {
446
+ throw unreadable("a channel or cast member");
447
+ }
404
448
  try {
405
449
  for (const schedule of scheduleRows(scratch))
406
450
  runsForSchedule(scratch, schedule.id);
@@ -428,7 +472,7 @@ function checkScratch(deps, scratch, library, parts) {
428
472
  }
429
473
  for (const project of parts) {
430
474
  try {
431
- if (projectById(scratch, project.id) === undefined)
475
+ if (projectByIdIncludingTrash(scratch, project.id) === undefined)
432
476
  throw new Error("project");
433
477
  for (const row of rowsOf(scratch, "SELECT id FROM project_revisions WHERE project_id=?", project.id))
434
478
  revisionById(scratch, project.id, String(row.id));
@@ -516,14 +560,14 @@ function commit(deps, work, manifest, prepared) {
516
560
  }
517
561
  const prompts = mergeNamed(db, scratch, {
518
562
  table: "prompts",
519
- sameName: "SELECT body FROM prompts WHERE kind=? AND lower(name)=lower(?)",
563
+ sameName: "SELECT body FROM prompts WHERE kind=? AND lower(name)=lower(?) AND deleted_at IS NULL",
520
564
  key: (row) => [row.kind ?? null],
521
565
  same: (row, existing) => existing.body === row.body,
522
566
  max: nameMax,
523
567
  });
524
568
  const entries = mergeNamed(db, scratch, {
525
569
  table: "entries",
526
- sameName: "SELECT mode,body FROM entries WHERE category=? AND lower(name)=lower(?)",
570
+ sameName: "SELECT mode,body FROM entries WHERE category=? AND lower(name)=lower(?) AND deleted_at IS NULL",
527
571
  key: (row) => [row.category ?? null],
528
572
  same: (row, existing) => existing.mode === row.mode && existing.body === row.body,
529
573
  max: nameMax,
@@ -535,6 +579,9 @@ function commit(deps, work, manifest, prepared) {
535
579
  same: (row, existing) => existing.values_json === row.values_json,
536
580
  max: documentThemeNameMax,
537
581
  });
582
+ mergeVersions(db, scratch, "prompt", prompts.inserted);
583
+ mergeVersions(db, scratch, "entry", entries.inserted);
584
+ const channelCounts = mergeChannels(db, scratch, work, prepared.images, now);
538
585
  let voicesAdded = 0;
539
586
  let voicesSkipped = 0;
540
587
  for (const row of rowsOf(scratch, "SELECT * FROM voices ORDER BY rowid")) {
@@ -546,6 +593,23 @@ function commit(deps, work, manifest, prepared) {
546
593
  voicesAdded += 1;
547
594
  }
548
595
  }
596
+ // Library → Aliases: an alias for a written form this install already has is kept as
597
+ // it is here; the others are added after this install's own, in the backup's order.
598
+ const narrationAliases = { added: 0, renamed: 0, skipped: 0 };
599
+ let position = Number(db.prepare("SELECT coalesce(max(position),-1) AS n FROM narration_aliases").get()?.n ?? -1);
600
+ for (const row of rowsOf(scratch, "SELECT * FROM narration_aliases ORDER BY position,rowid")) {
601
+ const caseSensitive = row.case_sensitive === 1;
602
+ if (has("SELECT 1 FROM narration_aliases WHERE id=?", String(row.id)) ||
603
+ has(caseSensitive
604
+ ? "SELECT 1 FROM narration_aliases WHERE case_sensitive=1 AND written=?"
605
+ : "SELECT 1 FROM narration_aliases WHERE case_sensitive=0 AND lower(written)=lower(?)", String(row.written)))
606
+ narrationAliases.skipped += 1;
607
+ else {
608
+ position += 1;
609
+ insertRow(db, "narration_aliases", { ...row, position });
610
+ narrationAliases.added += 1;
611
+ }
612
+ }
549
613
  const templates = { added: 0, renamed: 0, skipped: 0 };
550
614
  for (const template of rowsOf(scratch, "SELECT * FROM project_templates ORDER BY rowid")) {
551
615
  const id = String(template.id);
@@ -557,9 +621,11 @@ function commit(deps, work, manifest, prepared) {
557
621
  let renamed = false;
558
622
  for (const revision of rowsOf(scratch, "SELECT * FROM project_template_revisions WHERE template_id=? ORDER BY version", id)) {
559
623
  let next = revision;
560
- if (revision.version === template.head_version) {
624
+ // Names clash only among templates outside the trash, like prompts'.
625
+ if (revision.version === template.head_version &&
626
+ typeof template.deleted_at !== "string") {
561
627
  const name = String(revision.name);
562
- const taken = (candidate) => has("SELECT 1 FROM project_templates t JOIN project_template_revisions r ON r.template_id=t.id AND r.version=t.head_version WHERE t.id<>? AND lower(r.name)=lower(?)", id, candidate);
628
+ const taken = (candidate) => has("SELECT 1 FROM project_templates t JOIN project_template_revisions r ON r.template_id=t.id AND r.version=t.head_version WHERE t.id<>? AND t.deleted_at IS NULL AND lower(r.name)=lower(?)", id, candidate);
563
629
  if (taken(name)) {
564
630
  next = { ...revision, name: importedName(name, 200, taken) };
565
631
  renamed = true;
@@ -582,12 +648,19 @@ function commit(deps, work, manifest, prepared) {
582
648
  // Two installs running one schedule would make every project twice. It comes in
583
649
  // paused; Resume on the Schedules screen starts it here.
584
650
  const active = schedule.status === "active";
585
- insertRow(db, "schedules", active ? { ...schedule, status: "paused", updated_at: now } : schedule);
651
+ insertRow(db, "schedules",
652
+ // A generation the other install was running does not carry over.
653
+ active
654
+ ? { ...schedule, status: "paused", updated_at: now, topics_generating_at: null }
655
+ : { ...schedule, topics_generating_at: null });
586
656
  if (active)
587
657
  schedules.paused += 1;
588
658
  for (const run of rowsOf(scratch, "SELECT * FROM schedule_runs WHERE schedule_id=? ORDER BY rowid", String(schedule.id)))
589
659
  if (!has("SELECT 1 FROM schedule_runs WHERE id=?", String(run.id)))
590
660
  insertRow(db, "schedule_runs", run);
661
+ for (const topic of rowsOf(scratch, "SELECT * FROM schedule_topics WHERE schedule_id=? ORDER BY rowid", String(schedule.id)))
662
+ if (!has("SELECT 1 FROM schedule_topics WHERE id=?", String(topic.id)))
663
+ insertRow(db, "schedule_topics", topic);
591
664
  schedules.added += 1;
592
665
  }
593
666
  const drafts = { added: 0, renamed: 0, skipped: 0 };
@@ -624,6 +697,10 @@ function commit(deps, work, manifest, prepared) {
624
697
  fonts += 1;
625
698
  }
626
699
  const alreadyImported = has("SELECT 1 FROM backup_imports WHERE backup_id=?", manifest.backupId);
700
+ // Run cost made for a schedule or channel: a call already here, from an earlier import
701
+ // of the same backup, is kept once.
702
+ for (const row of rowsOf(scratch, "SELECT * FROM standalone_usage ORDER BY rowid"))
703
+ insertRow(db, "standalone_usage", row, "INSERT OR IGNORE");
627
704
  let events = 0;
628
705
  if (!alreadyImported)
629
706
  for (const event of rowsOf(scratch, "SELECT * FROM telemetry_events ORDER BY rowid")) {
@@ -662,9 +739,11 @@ function commit(deps, work, manifest, prepared) {
662
739
  appVersion: manifest.appVersion,
663
740
  },
664
741
  projects: { imported, skipped },
665
- prompts,
666
- entries,
742
+ prompts: counts(prompts),
743
+ entries: counts(entries),
744
+ ...channelCounts,
667
745
  documentThemes,
746
+ narrationAliases,
668
747
  templates,
669
748
  schedules,
670
749
  voices: { added: voicesAdded, renamed: 0, skipped: voicesSkipped },
@@ -694,10 +773,15 @@ function commit(deps, work, manifest, prepared) {
694
773
  // Library items whose name is unique per kind: same id is the same item and is skipped; the
695
774
  // same name with the same content is skipped; the same name with other content comes in as
696
775
  // "<name> (imported)".
776
+ //
777
+ // A prompt or intro/outro in the backup's trash comes in as it is, still in the trash: names
778
+ // only have to be unique among the items that are not (0039), and the name lookups here only
779
+ // see those.
697
780
  function mergeNamed(db, scratch, rule) {
698
781
  let added = 0;
699
782
  let renamed = 0;
700
783
  let skipped = 0;
784
+ const inserted = new Set();
701
785
  for (const row of rowsOf(scratch, `SELECT * FROM ${rule.table} ORDER BY rowid`)) {
702
786
  if (db.prepare(`SELECT 1 FROM ${rule.table} WHERE id=?`).get(String(row.id)) !== undefined) {
703
787
  skipped += 1;
@@ -705,9 +789,11 @@ function mergeNamed(db, scratch, rule) {
705
789
  }
706
790
  const name = String(row.name);
707
791
  const lookup = (candidate) => db.prepare(rule.sameName).get(...rule.key(row), candidate);
708
- const existing = lookup(name);
792
+ const trashed = typeof row.deleted_at === "string";
793
+ const existing = trashed ? undefined : lookup(name);
709
794
  if (existing === undefined) {
710
795
  insertRow(db, rule.table, row);
796
+ inserted.add(String(row.id));
711
797
  added += 1;
712
798
  }
713
799
  else if (rule.same(row, existing)) {
@@ -718,10 +804,96 @@ function mergeNamed(db, scratch, rule) {
718
804
  ...row,
719
805
  name: importedName(name, rule.max, (candidate) => lookup(candidate) !== undefined),
720
806
  });
807
+ inserted.add(String(row.id));
721
808
  renamed += 1;
722
809
  }
723
810
  }
724
- return { added, renamed, skipped };
811
+ return { added, renamed, skipped, inserted };
812
+ }
813
+ // A prompt's or intro/outro's History comes with it when the item itself came in; an item
814
+ // that was already here keeps the history it has.
815
+ function mergeVersions(db, scratch, itemKind, inserted) {
816
+ for (const row of rowsOf(scratch, "SELECT * FROM library_versions WHERE item_kind=? ORDER BY item_id,version", itemKind))
817
+ if (inserted.has(String(row.item_id)))
818
+ insertRow(db, "library_versions", row, "INSERT OR IGNORE");
819
+ }
820
+ // The default channel has the same id on every install (0032), so the backup's default is
821
+ // always "already here". While this install's own default is still as it was made (never
822
+ // renamed or edited), it takes the backup's name, brand kit, brief and settings; otherwise it
823
+ // is kept. Any other channel whose id is here is skipped. Cast members, their pictures,
824
+ // episode memories and existing videos then come in by id into whichever channel is here.
825
+ function mergeChannels(db, scratch, work, images, now) {
826
+ const has = (sql, ...params) => db.prepare(sql).get(...params) !== undefined;
827
+ const channels = { added: 0, renamed: 0, skipped: 0 };
828
+ for (const row of rowsOf(scratch, "SELECT * FROM channels ORDER BY rowid")) {
829
+ const id = String(row.id);
830
+ const local = db
831
+ .prepare("SELECT is_default,version,brand_json,series_brief FROM channels WHERE id=?")
832
+ .get(id);
833
+ if (local === undefined) {
834
+ insertRow(db, "channels", { ...row, is_default: 0 });
835
+ channels.added += 1;
836
+ }
837
+ else if (local.is_default === 1 &&
838
+ row.is_default === 1 &&
839
+ local.version === 1 &&
840
+ local.brand_json === "{}" &&
841
+ local.series_brief === "" &&
842
+ !(row.version === 1 && row.brand_json === "{}" && row.series_brief === "")) {
843
+ const columns = Object.keys(row).filter((column) => column !== "id" && column !== "is_default");
844
+ db.prepare(`UPDATE channels SET ${columns.map((column) => `${column}=?`).join(",")} WHERE id=?`).run(...columns.map((column) => row[column] ?? null), id);
845
+ channels.added += 1;
846
+ }
847
+ else
848
+ channels.skipped += 1;
849
+ }
850
+ for (const image of images) {
851
+ const path = join(work, "images", image.sha256);
852
+ if (!existsSync(path))
853
+ continue;
854
+ db.prepare("INSERT INTO image_blobs(sha256,mime,bytes,created_at) VALUES (?,?,?,?) ON CONFLICT(sha256) DO NOTHING").run(image.sha256, image.mime, readFileSync(path), now);
855
+ }
856
+ const cast = { added: 0, renamed: 0, skipped: 0 };
857
+ for (const row of rowsOf(scratch, "SELECT * FROM cast_members ORDER BY rowid")) {
858
+ if (has("SELECT 1 FROM cast_members WHERE id=?", String(row.id)))
859
+ cast.skipped += 1;
860
+ else {
861
+ insertRow(db, "cast_members", row);
862
+ cast.added += 1;
863
+ }
864
+ }
865
+ for (const row of rowsOf(scratch, "SELECT * FROM cast_images ORDER BY rowid"))
866
+ if (!has("SELECT 1 FROM cast_images WHERE id=?", String(row.id)) &&
867
+ has("SELECT 1 FROM cast_members WHERE id=?", String(row.member_id)) &&
868
+ (row.sha256 === null || has("SELECT 1 FROM image_blobs WHERE sha256=?", String(row.sha256))))
869
+ // A picture the other install was still drawing never finishes here.
870
+ insertRow(db, "cast_images", row.state === "generating"
871
+ ? { ...row, state: "failed", error: "Stopped by the backup. Make it again." }
872
+ : row);
873
+ const episodeMemories = { added: 0, renamed: 0, skipped: 0 };
874
+ for (const row of rowsOf(scratch, "SELECT * FROM episode_memories ORDER BY rowid")) {
875
+ if (has("SELECT 1 FROM episode_memories WHERE id=?", String(row.id)) ||
876
+ has("SELECT 1 FROM episode_memories WHERE project_id=?", String(row.project_id)))
877
+ episodeMemories.skipped += 1;
878
+ else {
879
+ insertRow(db, "episode_memories", row);
880
+ episodeMemories.added += 1;
881
+ }
882
+ }
883
+ const channelVideos = { added: 0, renamed: 0, skipped: 0 };
884
+ for (const row of rowsOf(scratch, "SELECT * FROM channel_videos ORDER BY rowid")) {
885
+ const changes = has("SELECT 1 FROM channel_videos WHERE id=?", String(row.id))
886
+ ? 0
887
+ : insertRow(db, "channel_videos", row, "INSERT OR IGNORE");
888
+ if (changes > 0)
889
+ channelVideos.added += 1;
890
+ else
891
+ channelVideos.skipped += 1;
892
+ }
893
+ return { channels, cast, episodeMemories, channelVideos };
894
+ }
895
+ function counts(value) {
896
+ return { added: value.added, renamed: value.renamed, skipped: value.skipped };
725
897
  }
726
898
  export function importedName(name, max, taken) {
727
899
  for (let n = 1;; n += 1) {
@@ -1,13 +1,16 @@
1
1
  import { rmSync } from "node:fs";
2
2
  import { derive } from "../../kernel/runner/graph.js";
3
- import { projectExists, stagesOf } from "../admission/repo.js";
3
+ import { stagesOf } from "../admission/repo.js";
4
4
  import { projectDir } from "./layout.js";
5
+ // A run in flight: a call the runner holds, or a stage running or waiting out a retry.
6
+ export function projectBusy(deps, projectId) {
7
+ return (deps.hasInflight?.(projectId) === true || derive(stagesOf(deps.db, projectId)) === "running");
8
+ }
5
9
  export function deleteProject(deps, projectId) {
6
- if (!projectExists(deps.db, projectId)) {
10
+ if (deps.db.prepare("SELECT 1 FROM projects WHERE id = ?").get(projectId) === undefined) {
7
11
  return { ok: false, reason: "no-project" };
8
12
  }
9
- if (deps.hasInflight?.(projectId) === true ||
10
- derive(stagesOf(deps.db, projectId)) === "running") {
13
+ if (projectBusy(deps, projectId)) {
11
14
  return { ok: false, reason: "running" };
12
15
  }
13
16
  // Files first, rows second. A folder that would not go leaves the project listed; rows
@@ -0,0 +1,79 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, posix, relative, resolve, win32 } from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execute = promisify(execFile);
7
+ export function nodeDocumentsHost() {
8
+ return {
9
+ platform: process.platform,
10
+ env: process.env,
11
+ home: homedir(),
12
+ run: async (command, args) => (await execute(command, [...args], { timeout: 10_000, windowsHide: true })).stdout,
13
+ read: (path) => readFile(path, "utf8").catch(() => undefined),
14
+ };
15
+ }
16
+ // The user's own Documents folder, the way the system itself names it: Windows asks for the
17
+ // known folder (which follows OneDrive and a folder moved in its Properties), Linux asks
18
+ // xdg-user-dir and then reads user-dirs.dirs, macOS has no setting for it. Never throws: the
19
+ // fallback is <home>/Documents. It isn't created here.
20
+ export async function documentsDir(host) {
21
+ if (host.platform === "win32") {
22
+ const known = await host
23
+ .run("powershell.exe", [
24
+ "-NoProfile",
25
+ "-NonInteractive",
26
+ "-Command",
27
+ "[Environment]::GetFolderPath('MyDocuments')",
28
+ ])
29
+ .then((out) => out.trim())
30
+ .catch(() => "");
31
+ if (usable(known, host.home, true))
32
+ return known;
33
+ return win32.join(host.env.USERPROFILE?.trim() || host.home, "Documents");
34
+ }
35
+ const fallback = posix.join(host.home, "Documents");
36
+ if (host.platform === "darwin")
37
+ return fallback;
38
+ const asked = await host
39
+ .run("xdg-user-dir", ["DOCUMENTS"])
40
+ .then((out) => out.trim())
41
+ .catch(() => "");
42
+ if (usable(asked, host.home))
43
+ return resolve(asked);
44
+ const configHome = host.env.XDG_CONFIG_HOME?.trim() || join(host.home, ".config");
45
+ const configured = userDirsDocuments((await host.read(join(configHome, "user-dirs.dirs"))) ?? "", host.home);
46
+ if (configured !== undefined && usable(configured, host.home))
47
+ return resolve(configured);
48
+ return fallback;
49
+ }
50
+ // XDG_DOCUMENTS_DIR from a user-dirs.dirs file: `XDG_DOCUMENTS_DIR="$HOME/Documents"`. The
51
+ // format allows only $HOME at the start or an absolute path.
52
+ export function userDirsDocuments(text, home) {
53
+ for (const line of text.split(/\r?\n/)) {
54
+ const match = /^\s*XDG_DOCUMENTS_DIR\s*=\s*"([^"]*)"\s*$/.exec(line);
55
+ if (match === null)
56
+ continue;
57
+ const value = match[1] ?? "";
58
+ if (value === "$HOME" || value.startsWith("$HOME/"))
59
+ return join(home, value.slice("$HOME".length));
60
+ if (isAbsolute(value))
61
+ return value;
62
+ return undefined;
63
+ }
64
+ return undefined;
65
+ }
66
+ // xdg-user-dir answers with the home folder itself when Documents isn't set (the spec's "not
67
+ // configured" value); a Documents folder that is the home folder would mix Slopify's folder
68
+ // with everything else, so that counts as unset too.
69
+ function usable(path, home, windows = false) {
70
+ if (path === "")
71
+ return false;
72
+ if (!windows && !isAbsolute(path))
73
+ return false;
74
+ if (windows && !/^(?:[a-zA-Z]:[\\/]|\\\\)/.test(path))
75
+ return false;
76
+ if (windows)
77
+ return win32.relative(win32.resolve(home), win32.resolve(path)) !== "";
78
+ return relative(resolve(home), resolve(path)) !== "";
79
+ }