@gentbajko/slopify 2.4.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 (469) 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 +11 -1
  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 +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -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-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { transact } from "../../kernel/db/tx.js";
3
3
  import { derive } from "../../kernel/runner/graph.js";
4
- import { projectPaused, stagesOf } from "../admission/repo.js";
4
+ import { liveProject, projectPaused, stagesOf } from "../admission/repo.js";
5
5
  import { startRun } from "../admission/start.js";
6
6
  import { deleteStagedFile } from "../storage/repo.js";
7
7
  import { releaseStagedFile } from "../storage/staging-refs.js";
@@ -11,10 +11,13 @@ const queueRow = z.object({
11
11
  position: z.number(),
12
12
  state: z.enum(["queued", "active", "finished"]),
13
13
  });
14
+ // A project in the trash leaves the queue's view, so the queue moves on past it; restored,
15
+ // it is back in its place.
14
16
  export function queueEntries(db, batchId) {
15
17
  return db
16
18
  .prepare(`SELECT project_id AS projectId, batch_id AS batchId, position, state
17
19
  FROM project_queue ${batchId === undefined ? "WHERE state != 'finished'" : "WHERE batch_id = ?"}
20
+ AND ${liveProject("project_queue", "project_id")}
18
21
  ORDER BY position`)
19
22
  .all(...(batchId === undefined ? [] : [batchId]))
20
23
  .map((row) => queueRow.parse(row));
@@ -48,6 +51,10 @@ export function enqueueBatch(deps, batchId, runs, retainStaged = false) {
48
51
  if (draft.sources.images === "provide")
49
52
  for (const id of draft.provided.images ?? [])
50
53
  used.add(id);
54
+ if (draft.shorts?.enabled === true && draft.provided.shortsMusic)
55
+ used.add(draft.provided.shortsMusic);
56
+ if (draft.ambientBed?.source === "upload" && draft.provided.ambientBed)
57
+ used.add(draft.provided.ambientBed);
51
58
  }
52
59
  for (const id of retainStaged ? [] : used) {
53
60
  sources.add(id);
@@ -66,7 +73,7 @@ export function pumpQueue(db, runner) {
66
73
  const status = derive(stages, projectPaused(db, entry.projectId));
67
74
  if (runner.hasInflight?.(entry.projectId))
68
75
  return;
69
- if (["done", "failed", "canceled"].includes(status)) {
76
+ if (["done", "partial", "failed", "canceled"].includes(status)) {
70
77
  db.prepare("UPDATE project_queue SET state = 'finished' WHERE project_id = ?").run(entry.projectId);
71
78
  continue;
72
79
  }
@@ -53,7 +53,7 @@ async function cancel(deps, projectId) {
53
53
  if (paused)
54
54
  setProjectPaused(deps.db, projectId, false, at);
55
55
  deps.db
56
- .prepare("UPDATE revision_work SET state='canceled',dispatch_state='held',failure_reason=? WHERE project_id=? AND state!='done'")
56
+ .prepare("UPDATE revision_work SET state='canceled',dispatch_state='held',failure_reason=?,retry_at=NULL WHERE project_id=? AND state!='done'")
57
57
  .run(canceledByUser, projectId);
58
58
  deps.db
59
59
  .prepare("UPDATE revision_work_pieces SET state='held',dispatch_state='held' WHERE work_id IN (SELECT id FROM revision_work WHERE project_id=?) AND state!='done'")
@@ -0,0 +1,110 @@
1
+ import { castImageMaxBytes, castImagesPerMember, sniffImage, storeImageBlob } from "./images.js";
2
+ import { castMemberById } from "./repo.js";
3
+ import { castGenerateSchema } from "./schema.js";
4
+ export function uploadCastImage(deps, memberId, bytes) {
5
+ const room = roomFor(deps, memberId);
6
+ if (!room.ok)
7
+ return room;
8
+ if (bytes.byteLength > castImageMaxBytes)
9
+ return {
10
+ ok: false,
11
+ reason: "not-an-image",
12
+ message: `This picture is larger than ${String(castImageMaxBytes / 1024 / 1024)} MB. Save it smaller (for example as a JPEG) and upload it again.`,
13
+ };
14
+ const mime = sniffImage(bytes);
15
+ if (mime === undefined)
16
+ return {
17
+ ok: false,
18
+ reason: "not-an-image",
19
+ message: "This file is not a PNG or JPEG picture. Upload a PNG or JPEG file.",
20
+ };
21
+ const at = deps.clock.now().toISOString();
22
+ const sha256 = storeImageBlob(deps.db, bytes, mime, at);
23
+ const id = deps.uuid();
24
+ deps.db
25
+ .prepare("INSERT INTO cast_images(id,member_id,source,prompt,state,error,sha256,created_at) VALUES (?,?,'upload',NULL,'ready',NULL,?,?)")
26
+ .run(id, memberId, sha256, at);
27
+ return { ok: true, value: imageRow(deps, id) };
28
+ }
29
+ // Adds a row in "generating" and returns it at once; the picture lands in the row when the
30
+ // provider answers, or the row turns "failed" with the reason. Characters and creatures are
31
+ // drawn upright, places and objects wide.
32
+ export function generateCastImage(deps, memberId, input) {
33
+ const parsed = castGenerateSchema.safeParse(input);
34
+ if (!parsed.success)
35
+ return { ok: false, reason: "invalid-input" };
36
+ const room = roomFor(deps, memberId);
37
+ if (!room.ok)
38
+ return room;
39
+ const generate = deps.generateImage;
40
+ if (generate === undefined)
41
+ return { ok: false, reason: "no-image-provider" };
42
+ const id = deps.uuid();
43
+ deps.db
44
+ .prepare("INSERT INTO cast_images(id,member_id,source,prompt,state,error,sha256,created_at) VALUES (?,?,'generate',?,'generating',NULL,NULL,?)")
45
+ .run(id, memberId, parsed.data.prompt, deps.clock.now().toISOString());
46
+ void generate({
47
+ ...parsed.data,
48
+ channelId: room.value.channelId,
49
+ aspect: aspectOf(room.value.kind),
50
+ }).then((image) => {
51
+ const mime = sniffImage(image.bytes);
52
+ if (mime === undefined || image.bytes.byteLength > castImageMaxBytes) {
53
+ fail(deps, id, "The provider answered with something that is not a usable PNG or JPEG picture. Press Generate again, or upload a picture instead.");
54
+ return;
55
+ }
56
+ const sha256 = storeImageBlob(deps.db, image.bytes, mime, deps.clock.now().toISOString());
57
+ deps.db
58
+ .prepare("UPDATE cast_images SET state='ready',sha256=? WHERE id=? AND state='generating'")
59
+ .run(sha256, id);
60
+ }, (error) => {
61
+ const reason = error instanceof Error ? error.message : String(error);
62
+ deps.log?.write("warn", "channels.cast-image.failed", { detail: reason.slice(0, 500) });
63
+ fail(deps, id, `The picture couldn't be made: ${reason.slice(0, 500)} Check the image provider in Settings → Providers, then press Generate again.`);
64
+ });
65
+ return { ok: true, value: imageRow(deps, id) };
66
+ }
67
+ export function deleteCastImage(deps, memberId, imageId) {
68
+ // The bytes stay: a project started with this picture still names it.
69
+ const changed = deps.db
70
+ .prepare("DELETE FROM cast_images WHERE id=? AND member_id=?")
71
+ .run(imageId, memberId);
72
+ return Number(changed.changes) === 0
73
+ ? { ok: false, reason: "not-found" }
74
+ : { ok: true, value: { deleted: true } };
75
+ }
76
+ // A picture still being made when Slopify stopped will never land.
77
+ export function settleInterruptedCastImages(deps) {
78
+ const changed = deps.db
79
+ .prepare("UPDATE cast_images SET state='failed',error='Slopify stopped before this picture was finished. Press Generate again.' WHERE state='generating'")
80
+ .run();
81
+ return Number(changed.changes);
82
+ }
83
+ function roomFor(deps, memberId) {
84
+ const member = castMemberById(deps.db, memberId);
85
+ if (member === undefined)
86
+ return { ok: false, reason: "not-found" };
87
+ if (member.images.filter((image) => image.state !== "failed").length >= castImagesPerMember)
88
+ return {
89
+ ok: false,
90
+ reason: "too-many-images",
91
+ message: `${member.name} already has ${String(castImagesPerMember)} pictures. Delete one before adding another.`,
92
+ };
93
+ return { ok: true, value: { kind: member.kind, channelId: member.channelId } };
94
+ }
95
+ function aspectOf(kind) {
96
+ return kind === "character" || kind === "creature" ? "9:16" : "16:9";
97
+ }
98
+ function fail(deps, id, error) {
99
+ deps.db
100
+ .prepare("UPDATE cast_images SET state='failed',error=? WHERE id=? AND state='generating'")
101
+ .run(error, id);
102
+ }
103
+ function imageRow(deps, id) {
104
+ const row = deps.db.prepare("SELECT member_id FROM cast_images WHERE id=?").get(id);
105
+ const member = castMemberById(deps.db, String(row?.member_id));
106
+ const image = member?.images.find((value) => value.id === id);
107
+ if (image === undefined)
108
+ throw new Error("The cast picture could not be read back");
109
+ return image;
110
+ }
@@ -0,0 +1,32 @@
1
+ // Which cast members a text mentions: a member's name or one of its aliases as a whole word,
2
+ // ignoring case. A word boundary is anything that is not a letter or digit in any script, so
3
+ // "Tiamat's lair" and "(Tiamat)" mention Tiamat and "Tiamatic" or "Tiamats" do not; a plural
4
+ // is an alias to add. Spaces inside a name match any run of whitespace. The members come back
5
+ // in the order the text first mentions them, then in cast order.
6
+ export function castMentions(text, cast) {
7
+ if (cast === undefined || cast.length === 0 || text.trim() === "")
8
+ return [];
9
+ const found = [];
10
+ for (const [order, member] of cast.entries()) {
11
+ let first = Number.POSITIVE_INFINITY;
12
+ for (const term of [member.name, ...member.aliases]) {
13
+ const at = mentionAt(text, term);
14
+ if (at !== undefined && at < first)
15
+ first = at;
16
+ }
17
+ if (Number.isFinite(first))
18
+ found.push({ member, at: first, order });
19
+ }
20
+ return found.sort((a, b) => a.at - b.at || a.order - b.order).map((row) => row.member);
21
+ }
22
+ function mentionAt(text, term) {
23
+ const words = term.trim().split(/\s+/u).filter(Boolean);
24
+ if (words.length === 0)
25
+ return undefined;
26
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}])${words.map(escapeRegExp).join("\\s+")}(?![\\p{L}\\p{N}])`, "iu");
27
+ const match = pattern.exec(text);
28
+ return match === null ? undefined : match.index;
29
+ }
30
+ function escapeRegExp(word) {
31
+ return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
+ }
@@ -0,0 +1,19 @@
1
+ import { createHash } from "node:crypto";
2
+ import { insertImageBlob } from "./repo.js";
3
+ // What a picture sent as a reference may weigh: the host helper's bridge carries at most this
4
+ // much per reference image (`bridgeLimits.reference`).
5
+ export const castImageMaxBytes = 10 * 1024 * 1024;
6
+ // Per cast member. Every one is sent with each image that mentions the member.
7
+ export const castImagesPerMember = 4;
8
+ export function sniffImage(bytes) {
9
+ const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
10
+ if (bytes.length >= png.length && png.every((value, index) => bytes[index] === value))
11
+ return "image/png";
12
+ return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff ? "image/jpeg" : undefined;
13
+ }
14
+ // Stores the bytes under their SHA-256 and returns it; the same picture twice is one row.
15
+ export function storeImageBlob(db, bytes, mime, at) {
16
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
17
+ insertImageBlob(db, { sha256, mime, bytes }, at);
18
+ return sha256;
19
+ }
@@ -0,0 +1,5 @@
1
+ // Every install has this channel (migration 0032). A template or project whose channel is
2
+ // NULL, or names a channel this install does not have, belongs to it.
3
+ export const defaultChannelId = "00000000-0000-4000-8000-000000000001";
4
+ export const defaultChannelName = "My channel";
5
+ export const castKinds = ["character", "creature", "place", "object"];
@@ -0,0 +1,100 @@
1
+ // A project moved to another channel in Edit project, or its brand kit switched on or off:
2
+ // the run takes the new channel's cast as it is now and its brand kit again, as Play does when
3
+ // a run starts (`runs.ts`). The kit fills what is at its default; a value the old channel's kit
4
+ // had put there is replaced by the new kit's, and one the person set themselves is kept.
5
+ // Nothing changes while the channel and the switch stay as they were, so saving any other
6
+ // edit leaves the project's cast and brand exactly as they were.
7
+ import { defaultDocumentTheme } from "../document/model.js";
8
+ import { entryByName } from "../library/repo.js";
9
+ import { channelById, resolveChannelId } from "./repo.js";
10
+ import { brandDocument, castSnapshot } from "./runs.js";
11
+ export function channelChanged(db, before, after) {
12
+ return resolveChannelId(db, before.channelId) !== resolveChannelId(db, after.channelId);
13
+ }
14
+ export function rebrandedEdit(db, base, edit) {
15
+ const oldId = resolveChannelId(db, base.channelId);
16
+ const newId = resolveChannelId(db, edit.config.channelId);
17
+ const oldKit = base.useBrandKit !== false;
18
+ const newKit = edit.config.useBrandKit !== false;
19
+ if (oldId === newId && oldKit === newKit)
20
+ return edit;
21
+ const oldBrand = oldKit ? (channelById(db, oldId)?.brand ?? {}) : {};
22
+ const newBrand = newKit ? (channelById(db, newId)?.brand ?? {}) : {};
23
+ const { useBrandKit: _kit, cast: _cast, ...rest } = edit.config;
24
+ let config = {
25
+ ...rest,
26
+ channelId: newId,
27
+ ...(newKit ? {} : { useBrandKit: false }),
28
+ };
29
+ // The cast goes with the channel; the brand kit switch never changes it.
30
+ const cast = oldId === newId ? edit.config.cast : castSnapshot(db, newId);
31
+ if (cast !== undefined && cast.length > 0)
32
+ config = { ...config, cast };
33
+ let content = edit.content;
34
+ if (config.subtitles !== undefined) {
35
+ const subtitles = { ...config.subtitles };
36
+ const font = swapped(subtitles.fontId, oldBrand.captionFontId, newBrand.captionFontId, "default");
37
+ subtitles.fontId = font ?? "default";
38
+ assign(subtitles, "color", swapped(subtitles.color, oldBrand.captionColor, newBrand.captionColor));
39
+ assign(subtitles, "outlineColor", swapped(subtitles.outlineColor, oldBrand.captionOutlineColor, newBrand.captionOutlineColor));
40
+ config = { ...config, subtitles };
41
+ }
42
+ const titleStyle = {};
43
+ assign(titleStyle, "fontId", swapped(config.titleStyle?.fontId, oldBrand.titleFontId, newBrand.titleFontId));
44
+ assign(titleStyle, "color", swapped(config.titleStyle?.color, oldBrand.titleColor, newBrand.titleColor));
45
+ const { titleStyle: _title, endScreen: _end, ...unstyled } = config;
46
+ const endScreen = swapped(config.endScreen?.text, oldBrand.endScreenText, newBrand.endScreenText);
47
+ config = {
48
+ ...unstyled,
49
+ ...(Object.keys(titleStyle).length === 0 ? {} : { titleStyle }),
50
+ ...(endScreen === undefined ? {} : { endScreen: { text: endScreen } }),
51
+ };
52
+ // An intro or outro is read only while narration is generated.
53
+ if (config.sources.audio === "generate")
54
+ for (const category of ["intro", "outro"]) {
55
+ const current = config[category]?.name;
56
+ const old = oldBrand[category];
57
+ const fromOldKit = current === undefined || (old !== undefined && current.toLowerCase() === old.toLowerCase());
58
+ if (!fromOldKit)
59
+ continue;
60
+ const name = newBrand[category];
61
+ const entry = name === undefined ? undefined : entryByName(db, category, name);
62
+ if (entry === undefined) {
63
+ if (current === undefined)
64
+ continue;
65
+ const { [category]: _dropped, ...without } = config;
66
+ config = without;
67
+ continue;
68
+ }
69
+ config = { ...config, [category]: { name: entry.name, mode: entry.mode } };
70
+ content = {
71
+ ...content,
72
+ promptTemplates: { ...content.promptTemplates, [category]: entry.body },
73
+ };
74
+ }
75
+ // The PDF's theme, while the project makes one.
76
+ if (config.document !== undefined) {
77
+ const identity = config.document.custom?.id ?? config.document.theme;
78
+ const fromOldKit = identity === (oldBrand.documentTheme ?? defaultDocumentTheme) ||
79
+ (config.document.custom === undefined && config.document.theme === defaultDocumentTheme);
80
+ if (fromOldKit) {
81
+ const document = brandDocument(db, newBrand) ?? {
82
+ theme: defaultDocumentTheme,
83
+ };
84
+ config = { ...config, document };
85
+ }
86
+ }
87
+ return { ...edit, config, content };
88
+ }
89
+ // The value a brand-kit setting takes: the new kit's when the current one is unset, the
90
+ // fallback, or what the old kit put there; otherwise the person's own, kept.
91
+ function swapped(current, old, next, fallback) {
92
+ const fromOldKit = current === undefined || current === fallback || (old !== undefined && current === old);
93
+ return fromOldKit ? (next ?? fallback) : current;
94
+ }
95
+ function assign(target, key, value) {
96
+ if (value === undefined)
97
+ delete target[key];
98
+ else
99
+ target[key] = value;
100
+ }
@@ -0,0 +1,218 @@
1
+ import { z } from "zod";
2
+ import { aiDisclosureSettings } from "../studio/disclosure.js";
3
+ import { channelAmbientBedSchema } from "../video/ambient-bed-schema.js";
4
+ import { castKinds, defaultChannelId, } from "./model.js";
5
+ const brandSchema = z
6
+ .object({
7
+ captionFontId: z.string().optional(),
8
+ captionColor: z.string().optional(),
9
+ captionOutlineColor: z.string().optional(),
10
+ titleFontId: z.string().optional(),
11
+ titleColor: z.string().optional(),
12
+ intro: z.string().optional(),
13
+ outro: z.string().optional(),
14
+ endScreenText: z.string().optional(),
15
+ documentTheme: z.string().optional(),
16
+ ambientBed: channelAmbientBedSchema.optional(),
17
+ language: z.string().optional(),
18
+ })
19
+ .readonly();
20
+ const channelRow = z.object({
21
+ id: z.string(),
22
+ name: z.string(),
23
+ is_default: z.number(),
24
+ brand_json: z.string(),
25
+ series_brief: z.string(),
26
+ ai_disclosure: z.enum(aiDisclosureSettings),
27
+ version: z.number(),
28
+ created_at: z.string(),
29
+ updated_at: z.string(),
30
+ });
31
+ const memberRow = z.object({
32
+ id: z.string(),
33
+ channel_id: z.string(),
34
+ kind: z.enum(castKinds),
35
+ name: z.string(),
36
+ aliases_json: z.string(),
37
+ description: z.string(),
38
+ voice_json: z.string().nullable().optional(),
39
+ version: z.number(),
40
+ created_at: z.string(),
41
+ updated_at: z.string(),
42
+ });
43
+ const voiceJson = z.object({
44
+ provider: z.string(),
45
+ model: z.string(),
46
+ voice: z.string(),
47
+ pace: z.number().optional(),
48
+ pronunciations: z.string().optional(),
49
+ });
50
+ const imageRow = z.object({
51
+ id: z.string(),
52
+ member_id: z.string(),
53
+ source: z.enum(["upload", "generate"]),
54
+ prompt: z.string().nullable(),
55
+ state: z.enum(["ready", "generating", "failed"]),
56
+ error: z.string().nullable(),
57
+ sha256: z.string().nullable(),
58
+ created_at: z.string(),
59
+ });
60
+ function channelOf(row) {
61
+ const value = channelRow.parse(row);
62
+ return {
63
+ id: value.id,
64
+ name: value.name,
65
+ isDefault: value.is_default === 1,
66
+ brand: brandSchema.parse(JSON.parse(value.brand_json)),
67
+ seriesBrief: value.series_brief,
68
+ aiDisclosure: value.ai_disclosure,
69
+ version: value.version,
70
+ createdAt: value.created_at,
71
+ updatedAt: value.updated_at,
72
+ };
73
+ }
74
+ export function channelById(db, id) {
75
+ const row = db.prepare("SELECT * FROM channels WHERE id=?").get(id);
76
+ return row === undefined ? undefined : channelOf(row);
77
+ }
78
+ // A channel id as stored on a template or project: NULL or one this install does not have
79
+ // reads as the default channel.
80
+ export function resolveChannelId(db, id) {
81
+ if (id === null || id === undefined)
82
+ return defaultChannelId;
83
+ return db.prepare("SELECT 1 FROM channels WHERE id=?").get(id) === undefined
84
+ ? defaultChannelId
85
+ : id;
86
+ }
87
+ export function channelSummaries(db) {
88
+ const templates = counts(db, "SELECT coalesce(channel_id,'') AS id, count(*) AS n FROM project_templates WHERE deleted_at IS NULL GROUP BY channel_id");
89
+ const cast = counts(db, "SELECT channel_id AS id, count(*) AS n FROM cast_members GROUP BY channel_id");
90
+ const rows = db
91
+ .prepare("SELECT * FROM channels ORDER BY is_default DESC, lower(name), id")
92
+ .all()
93
+ .map(channelOf);
94
+ const known = new Set(rows.map((row) => row.id));
95
+ // Templates naming no channel, or one that is gone, are the default channel's.
96
+ const stray = [...templates.entries()]
97
+ .filter(([id]) => !known.has(id))
98
+ .reduce((sum, [, n]) => sum + n, 0);
99
+ return rows.map((row) => ({
100
+ ...row,
101
+ templates: (templates.get(row.id) ?? 0) + (row.isDefault ? stray : 0),
102
+ cast: cast.get(row.id) ?? 0,
103
+ }));
104
+ }
105
+ function counts(db, sql) {
106
+ const result = new Map();
107
+ for (const row of db.prepare(sql).all())
108
+ result.set(String(row.id), Number(row.n));
109
+ return result;
110
+ }
111
+ export function castOfChannel(db, channelId) {
112
+ const members = db
113
+ .prepare("SELECT * FROM cast_members WHERE channel_id=? ORDER BY lower(name), id")
114
+ .all(channelId)
115
+ .map((row) => memberRow.parse(row));
116
+ const images = new Map();
117
+ for (const row of db
118
+ .prepare("SELECT i.* FROM cast_images i JOIN cast_members m ON m.id=i.member_id WHERE m.channel_id=? ORDER BY i.created_at, i.rowid")
119
+ .all(channelId)) {
120
+ const image = imageRow.parse(row);
121
+ const list = images.get(image.member_id) ?? [];
122
+ list.push(imageOf(image));
123
+ images.set(image.member_id, list);
124
+ }
125
+ return members.map((row) => memberOf(row, images.get(row.id) ?? []));
126
+ }
127
+ export function castMemberById(db, id) {
128
+ const row = db.prepare("SELECT * FROM cast_members WHERE id=?").get(id);
129
+ if (row === undefined)
130
+ return undefined;
131
+ const images = db
132
+ .prepare("SELECT * FROM cast_images WHERE member_id=? ORDER BY created_at, rowid")
133
+ .all(id)
134
+ .map((value) => imageOf(imageRow.parse(value)));
135
+ return memberOf(memberRow.parse(row), images);
136
+ }
137
+ function memberOf(row, images) {
138
+ return {
139
+ id: row.id,
140
+ channelId: row.channel_id,
141
+ kind: row.kind,
142
+ name: row.name,
143
+ aliases: z.array(z.string()).parse(JSON.parse(row.aliases_json)),
144
+ description: row.description,
145
+ ...(row.voice_json === null || row.voice_json === undefined
146
+ ? {}
147
+ : { voice: voiceJson.parse(JSON.parse(row.voice_json)) }),
148
+ version: row.version,
149
+ images,
150
+ createdAt: row.created_at,
151
+ updatedAt: row.updated_at,
152
+ };
153
+ }
154
+ function imageOf(row) {
155
+ return {
156
+ id: row.id,
157
+ source: row.source,
158
+ prompt: row.prompt,
159
+ state: row.state,
160
+ error: row.error,
161
+ sha256: row.sha256,
162
+ createdAt: row.created_at,
163
+ };
164
+ }
165
+ export function templateChannelId(db, templateId) {
166
+ const row = db.prepare("SELECT channel_id FROM project_templates WHERE id=?").get(templateId);
167
+ return resolveChannelId(db, typeof row?.channel_id === "string" ? row.channel_id : null);
168
+ }
169
+ // Every template's channel, for the lists that show or filter by it.
170
+ export function templateChannels(db) {
171
+ const result = new Map();
172
+ for (const row of db.prepare("SELECT id, channel_id FROM project_templates").all())
173
+ result.set(String(row.id), resolveChannelId(db, typeof row.channel_id === "string" ? row.channel_id : null));
174
+ return result;
175
+ }
176
+ // A schedule belongs to the channel of the template it runs, so moving a template moves its
177
+ // schedules with it and the series brief a schedule reads is always its channel's.
178
+ export function scheduleChannel(db, scheduleId) {
179
+ const row = db.prepare("SELECT template_id FROM schedules WHERE id=?").get(scheduleId);
180
+ if (typeof row?.template_id !== "string")
181
+ return undefined;
182
+ return channelById(db, templateChannelId(db, row.template_id));
183
+ }
184
+ export function projectChannelId(db, projectId) {
185
+ const row = db
186
+ .prepare("SELECT channel_id FROM project_channels WHERE project_id=?")
187
+ .get(projectId);
188
+ return resolveChannelId(db, typeof row?.channel_id === "string" ? row.channel_id : null);
189
+ }
190
+ // Every project's channel in one read, for the lists that show or filter by it. A project
191
+ // with no row, or naming a channel this install doesn't have, is in the default channel.
192
+ export function projectChannels(db) {
193
+ const known = new Set(db
194
+ .prepare("SELECT id FROM channels")
195
+ .all()
196
+ .map((row) => String(row.id)));
197
+ const result = new Map();
198
+ for (const row of db.prepare("SELECT project_id, channel_id FROM project_channels").all()) {
199
+ const channel = String(row.channel_id);
200
+ result.set(String(row.project_id), known.has(channel) ? channel : defaultChannelId);
201
+ }
202
+ return result;
203
+ }
204
+ export function setProjectChannel(db, projectId, channelId) {
205
+ db.prepare("INSERT INTO project_channels(project_id,channel_id) VALUES (?,?) ON CONFLICT(project_id) DO UPDATE SET channel_id=excluded.channel_id").run(projectId, channelId);
206
+ }
207
+ export function imageBlob(db, sha256) {
208
+ const row = db.prepare("SELECT mime, bytes FROM image_blobs WHERE sha256=?").get(sha256);
209
+ if (row === undefined)
210
+ return undefined;
211
+ const mime = z.enum(["image/png", "image/jpeg"]).parse(row.mime);
212
+ if (!(row.bytes instanceof Uint8Array))
213
+ return undefined;
214
+ return { bytes: new Uint8Array(row.bytes), mime };
215
+ }
216
+ export function insertImageBlob(db, blob, at) {
217
+ db.prepare("INSERT INTO image_blobs(sha256,mime,bytes,created_at) VALUES (?,?,?,?) ON CONFLICT(sha256) DO NOTHING").run(blob.sha256, blob.mime, blob.bytes, at);
218
+ }
@@ -0,0 +1,129 @@
1
+ import { languageSchema } from "../../kernel/ports/languages.js";
2
+ import { listDocumentThemes } from "../document/library.js";
3
+ import { documentThemes } from "../document/model.js";
4
+ import { ambientBedFormOf } from "../video/ambient-bed.js";
5
+ import { withCastVoices } from "../voices/cast.js";
6
+ import { castMemberById, castOfChannel, channelById, resolveChannelId, templateChannelId, } from "./repo.js";
7
+ // The channel a Play draft runs in: the one picked on Play, else its template's, else the
8
+ // default channel.
9
+ export function draftChannel(db, document) {
10
+ const id = document.channelId !== undefined
11
+ ? resolveChannelId(db, document.channelId)
12
+ : document.templateSource !== undefined
13
+ ? templateChannelId(db, document.templateSource.id)
14
+ : resolveChannelId(db, undefined);
15
+ const channel = channelById(db, id);
16
+ if (channel === undefined)
17
+ throw new Error("The default channel is missing");
18
+ return channel;
19
+ }
20
+ // The brand kit fills what the draft leaves at its default: the "default" caption font, no
21
+ // intro or outro, no document theme chosen, no ambient sound chosen. A template that sets its own keeps it, and one
22
+ // with "Use the channel's brand kit" off takes nothing from it. An intro, outro or theme the
23
+ // kit names that no longer exists is skipped, not refused: the draft never named it.
24
+ export function brandedForm(db, form, brand, entries) {
25
+ // The channel's language is not styling, so it applies with the brand kit off too; a
26
+ // language picked on Play (English included) is the draft's own.
27
+ const language = form.language === undefined && brand.language !== undefined
28
+ ? { language: languageSchema.parse(brand.language) }
29
+ : {};
30
+ if (form.useBrandKit === false)
31
+ return { ...form, ...language };
32
+ const entry = (category, name) => name === undefined
33
+ ? undefined
34
+ : entries.find((row) => row.category === category && row.name.toLowerCase() === name.toLowerCase())?.name;
35
+ const intro = form.intro === "" ? entry("intro", brand.intro) : undefined;
36
+ const outro = form.outro === "" ? entry("outro", brand.outro) : undefined;
37
+ const document = form.document === undefined ? brandDocument(db, brand) : undefined;
38
+ return {
39
+ ...form,
40
+ ...language,
41
+ ...(form.subtitles.fontId === "default" && brand.captionFontId !== undefined
42
+ ? { subtitles: { ...form.subtitles, fontId: brand.captionFontId } }
43
+ : {}),
44
+ ...(intro === undefined ? {} : { intro }),
45
+ ...(outro === undefined ? {} : { outro }),
46
+ ...(document === undefined ? {} : { document }),
47
+ // A setup that never touched its ambient sound takes the channel's; one set to None keeps
48
+ // none.
49
+ ...(form.ambientBed === undefined && brand.ambientBed !== undefined
50
+ ? { ambientBed: ambientBedFormOf(brand.ambientBed) }
51
+ : {}),
52
+ };
53
+ }
54
+ export function brandDocument(db, brand) {
55
+ const name = brand.documentTheme;
56
+ if (name === undefined)
57
+ return undefined;
58
+ const builtIn = documentThemes.find((theme) => theme === name);
59
+ if (builtIn !== undefined)
60
+ return { theme: builtIn };
61
+ const saved = listDocumentThemes(db).find((theme) => theme.id === name);
62
+ return saved === undefined
63
+ ? undefined
64
+ : { theme: "plain", custom: { id: saved.id, name: saved.name, values: saved.values } };
65
+ }
66
+ // What the run carries from its channel beyond the form: the channel itself, the caption and
67
+ // title colours, the end screen, and the cast. Each only when set, so a run from a channel
68
+ // with an empty kit and no cast is the run it always was.
69
+ export function brandedRun(draft, channel, cast, useBrandKit) {
70
+ const brand = useBrandKit ? channel.brand : {};
71
+ const colours = brand.captionColor === undefined && brand.captionOutlineColor === undefined
72
+ ? {}
73
+ : {
74
+ ...(brand.captionColor === undefined ? {} : { color: brand.captionColor }),
75
+ ...(brand.captionOutlineColor === undefined
76
+ ? {}
77
+ : { outlineColor: brand.captionOutlineColor }),
78
+ };
79
+ const titleStyle = {
80
+ ...(brand.titleFontId === undefined ? {} : { fontId: brand.titleFontId }),
81
+ ...(brand.titleColor === undefined ? {} : { color: brand.titleColor }),
82
+ };
83
+ return {
84
+ ...draft,
85
+ channelId: channel.id,
86
+ // Kept only when off, so a run with the kit on saves the config it always did.
87
+ ...(useBrandKit ? {} : { useBrandKit: false }),
88
+ ...(draft.subtitles !== undefined && Object.keys(colours).length > 0
89
+ ? { subtitles: { ...colours, ...draft.subtitles } }
90
+ : {}),
91
+ ...(Object.keys(titleStyle).length === 0 || draft.titleStyle !== undefined
92
+ ? {}
93
+ : { titleStyle }),
94
+ ...(brand.endScreenText === undefined || draft.endScreen !== undefined
95
+ ? {}
96
+ : { endScreen: { text: brand.endScreenText } }),
97
+ ...(cast.length === 0 ? {} : { cast }),
98
+ };
99
+ }
100
+ // The channel's cast as a run is started with it: members with at least one finished picture.
101
+ export function castSnapshot(db, channelId) {
102
+ return castOfChannel(db, channelId).flatMap((member) => {
103
+ const images = member.images.flatMap((image) => image.state === "ready" && image.sha256 !== null ? [image.sha256] : []);
104
+ return images.length === 0
105
+ ? []
106
+ : [
107
+ {
108
+ name: member.name,
109
+ aliases: member.aliases,
110
+ description: member.description,
111
+ images,
112
+ },
113
+ ];
114
+ });
115
+ }
116
+ // Speakers picked from the cast speak with the cast's voices (and show its portraits) as they
117
+ // are when the run starts. Every door a run starts through - Play, a schedule, a batch, the
118
+ // API - passes its draft through here once; a member is found by id, whichever channel it is
119
+ // on, and a draft without cast speakers comes back unchanged.
120
+ export function castVoicedRun(db, draft) {
121
+ if (draft.voices === undefined)
122
+ return draft;
123
+ const ids = new Set(draft.voices.speakers.flatMap((one) => one.castId ?? []));
124
+ if (ids.size === 0)
125
+ return draft;
126
+ const members = [...ids].flatMap((id) => castMemberById(db, id) ?? []);
127
+ const voices = withCastVoices(draft.voices, members);
128
+ return voices === draft.voices ? draft : { ...draft, voices };
129
+ }