@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
@@ -2,8 +2,30 @@ import { z } from "zod";
2
2
  import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultZoomPercent, edgeSilenceSecondsProblem, imageSecondsProblem, numberPerPromptMax, zoomPercentProblem, } from "../admission/rules.js";
3
3
  import { runDraftSchema } from "../admission/schema.js";
4
4
  import { draftDocumentThemeOf } from "../document/model.js";
5
+ import { defaultExpectedWords, everyMinutesMax, everyMinutesMin, expectedWordsMax, imagesPerHourMax, imagesPerHourMin, perHourFromMinutes, wordCount, } from "../images/scale.js";
6
+ import { defaultLoudness, loudnessOfForm, } from "../loudness/model.js";
7
+ import { pausesOfForm } from "../narration/pauses-model.js";
8
+ import { reviewSettingsFromForm } from "../reviews/model.js";
9
+ import { stageMakesItems } from "../reviews/rules.js";
5
10
  import { defaultShorts, shortsExtrasOf, shortsSettingsProblems, } from "../shorts/model.js";
6
11
  import { defaultSubtitles } from "../subtitles/model.js";
12
+ import { ambientBedOfForm, ambientBedProblems } from "../video/ambient-bed.js";
13
+ // Said where the control is, since the music sits in a closed disclosure on the Export rail.
14
+ const musicMessages = {
15
+ uploading: "The shorts' background music is still uploading, so the run can't start yet. Wait for it to finish, or remove it under Outputs → Export → More shorts options → Background music.",
16
+ missing: "The shorts' background music file is no longer available (it was not uploaded again after the draft was copied, or the upload failed). Choose the file again under Outputs → Export → More shorts options → Background music, or remove it.",
17
+ };
18
+ function pickMusic(id) {
19
+ return id === undefined ? {} : { shortsMusic: id };
20
+ }
21
+ const bedWhere = "under Outputs → Export → Ambient sound";
22
+ const bedMessages = {
23
+ uploading: `The ambient sound's audio file is still uploading, so the run can't start yet. Wait for it to finish ${bedWhere}.`,
24
+ missing: `The ambient sound's audio file is missing (it was never chosen, was not uploaded again after the draft was copied, or the upload failed). Choose the file ${bedWhere}, or pick Rain, Fireplace or Wind instead.`,
25
+ };
26
+ function pickBed(id) {
27
+ return id === undefined ? {} : { ambientBed: id };
28
+ }
7
29
  export function toAdmissionDraft(input) {
8
30
  const { form } = input.document;
9
31
  const fields = [];
@@ -49,15 +71,18 @@ export function toAdmissionDraft(input) {
49
71
  });
50
72
  return undefined;
51
73
  };
52
- const file = (ref, kind, field) => {
74
+ const file = (ref, kind, field, messages = {
75
+ uploading: "This file is still uploading. Wait for it to finish.",
76
+ missing: "That upload is no longer available. Choose the file again.",
77
+ }) => {
53
78
  const attachment = input.attachments.find((item) => item.id === ref?.attachmentId && item.kind === kind && item.name === ref.name);
54
79
  if (attachment?.state === "ready" && attachment.stagedFileId !== null)
55
80
  return attachment.stagedFileId;
56
81
  fields.push({
57
82
  field,
58
83
  message: attachment?.state === "copying" || attachment?.state === "pending"
59
- ? "This file is still uploading. Wait for it to finish."
60
- : "That upload is no longer available. Choose the file again.",
84
+ ? messages.uploading
85
+ : messages.missing,
61
86
  });
62
87
  return undefined;
63
88
  };
@@ -97,7 +122,28 @@ export function toAdmissionDraft(input) {
97
122
  : defaultShorts.maxSeconds,
98
123
  };
99
124
  };
125
+ // Only the reviews of stages that make something; none left is no reviews at all, which is
126
+ // what every draft saved before them was.
127
+ const reviewed = form.reviews === undefined
128
+ ? undefined
129
+ : reviewSettingsFromForm(form.reviews, (stage) => stageMakesItems({ sources, shorts: shortsOn ? { enabled: true } : undefined }, stage));
130
+ if (reviewed?.retriesProblem !== undefined)
131
+ fields.push({ field: "reviews.retries", message: reviewed.retriesProblem });
132
+ const reviews = reviewed?.settings;
133
+ // The ambient bed lies under the long video's narration, so a bed left set with either Off
134
+ // asks for nothing. Refused in the rule's words, so Play and the run say the same sentence.
135
+ const bed = sources.video === "generate" && sources.audio !== "off"
136
+ ? ambientBedOfForm(form.ambientBed)
137
+ : undefined;
138
+ const bedProblems = bed === undefined ? [] : ambientBedProblems(bed);
139
+ for (const problem of bedProblems)
140
+ fields.push({
141
+ field: `ambientBed.${problem.field}`,
142
+ message: `${problem.message} Change it ${bedWhere}.`,
143
+ });
144
+ const bedRefused = bedProblems.length;
100
145
  const draft = {
146
+ ...(reviews === undefined ? {} : { reviews }),
101
147
  ...(form.checkpoints === undefined ? {} : { checkpoints: form.checkpoints }),
102
148
  title: form.title,
103
149
  format: form.format,
@@ -112,17 +158,46 @@ export function toAdmissionDraft(input) {
112
158
  : {}),
113
159
  llm: form.llm,
114
160
  audio: sources.audio === "generate" || form.audio.usePronunciationGlossary !== undefined
115
- ? // With the glossary on, sharing it is on unless the draft turned it off, as Play
116
- // shows it for a draft saved before the switch existed.
117
- form.audio.usePronunciationGlossary === true && form.audio.shareGlossary === undefined
118
- ? { ...form.audio, shareGlossary: true }
119
- : form.audio
161
+ ? onlyWhenOn(
162
+ // With the glossary on, sharing it is on unless the draft turned it off, as Play
163
+ // shows it for a draft saved before the switch existed.
164
+ form.audio.usePronunciationGlossary === true && form.audio.shareGlossary === undefined
165
+ ? { ...form.audio, shareGlossary: true }
166
+ : form.audio)
120
167
  : undefined,
121
168
  images: form.images,
169
+ // The establishing image belongs to generated images; Off, or images not generated,
170
+ // leaves it out, which is what every draft saved before it was.
171
+ ...(sources.images === "generate" &&
172
+ form.reference !== undefined &&
173
+ form.reference.source !== "off"
174
+ ? {
175
+ reference: {
176
+ source: form.reference.source,
177
+ ...(form.reference.source === "prompt" ? { prompt: form.reference.prompt } : {}),
178
+ thumbnail: form.reference.thumbnail,
179
+ },
180
+ }
181
+ : {}),
182
+ // Three only for a thumbnail the image provider draws; one leaves it out, which is what
183
+ // every draft saved before it was.
184
+ ...(form.thumbnailCount === 3 &&
185
+ (sources.thumbnail === "from_prompt" || sources.thumbnail === "prompt_by_llm")
186
+ ? { thumbnailCount: 3 }
187
+ : {}),
122
188
  articlePrompt: sources.article === "generate" ? form.articlePrompt : undefined,
123
189
  ...(sources.audio === "generate" && form.narrationPrompt?.trim()
124
190
  ? { narrationPrompt: form.narrationPrompt }
125
191
  : {}),
192
+ // English is never stored on the run, so an English project is the run it always was.
193
+ ...(form.language === undefined || form.language === "en" ? {} : { language: form.language }),
194
+ // Drawn for the blocks the narration describes, so only with a video and describing on.
195
+ ...(sources.video === "generate" &&
196
+ sources.audio === "generate" &&
197
+ form.audio.describeFigures === true &&
198
+ form.showFigures === true
199
+ ? { showFigures: true }
200
+ : {}),
126
201
  // Timed from the narration, so a switch left on with narration Off asks for nothing.
127
202
  ...(sources.audio !== "off" && form.youtubeDescription === true
128
203
  ? {
@@ -139,6 +214,9 @@ export function toAdmissionDraft(input) {
139
214
  number: number(prompt.number, `imagePrompts.${index}.number`, numberPerPromptMax),
140
215
  }))
141
216
  : [],
217
+ ...(sources.images === "generate" && form.imageScale !== undefined
218
+ ? { imageScale: imageScaleOf(input.document, fields) }
219
+ : {}),
142
220
  thumbnailPrompt: ["from_prompt", "prompt_by_llm"].includes(sources.thumbnail)
143
221
  ? form.thumbnailPrompt
144
222
  : undefined,
@@ -154,12 +232,24 @@ export function toAdmissionDraft(input) {
154
232
  thumbnail: sources.thumbnail === "provide"
155
233
  ? file(form.provided.thumbnail, "thumbnail", "provided.thumbnail")
156
234
  : undefined,
235
+ reference: sources.images === "generate" && form.reference?.source === "provide"
236
+ ? file(form.provided.reference ?? null, "reference", "provided.reference")
237
+ : undefined,
157
238
  images: sources.images === "provide"
158
239
  ? form.provided.images.flatMap((ref, index) => {
159
240
  const id = file(ref, "images", `provided.images.${index}`);
160
241
  return id === undefined ? [] : [id];
161
242
  })
162
243
  : [],
244
+ // Optional, so only a file that was attached is checked; with Shorts off it is kept on
245
+ // the draft for when Shorts is turned back on, and never reaches the run.
246
+ ...(shortsOn && form.provided.shortsMusic
247
+ ? pickMusic(file(form.provided.shortsMusic, "audio", "shorts.music", musicMessages))
248
+ : {}),
249
+ // Only while the bed plays the user's own file; kept on the draft otherwise.
250
+ ...(bed?.source === "upload"
251
+ ? pickBed(file(form.provided.ambientBed ?? null, "audio", "ambientBed.file", bedMessages))
252
+ : {}),
163
253
  },
164
254
  chunking: {
165
255
  mode: chunkMode,
@@ -177,7 +267,11 @@ export function toAdmissionDraft(input) {
177
267
  mode,
178
268
  fontSize: number(form.subtitles.fontSize, "subtitles.fontSize", 120, 16),
179
269
  },
180
- silenceGapSeconds: input.silenceGapSeconds,
270
+ // This run's own gap when one was typed, else the one Settings has. Text that is not a
271
+ // number is NaN, which admission refuses at silenceGapSeconds.
272
+ silenceGapSeconds: form.silenceGapSeconds === undefined || form.silenceGapSeconds.trim() === ""
273
+ ? input.silenceGapSeconds
274
+ : Number(form.silenceGapSeconds),
181
275
  imageSeconds: measure("imageSeconds", sources.video === "generate" || shortsOn, defaultImageSeconds, imageSecondsProblem),
182
276
  zoomPercent: measure("zoomPercent", sources.video === "generate", defaultZoomPercent, zoomPercentProblem),
183
277
  // A choice from a list, so there is nothing to refuse; a run without a video keeps it
@@ -185,6 +279,15 @@ export function toAdmissionDraft(input) {
185
279
  motionStyle: form.motionStyle,
186
280
  // Kept the same way; admission checks it only while the video renders.
187
281
  ...(form.videoEdit === undefined ? {} : { videoEdit: form.videoEdit }),
282
+ // Spoken by the speakers only while narration is generated.
283
+ ...(form.voices !== undefined && sources.audio === "generate" ? { voices: form.voices } : {}),
284
+ // Under the long video's narration only; a number already refused above leaves it out
285
+ // rather than saved as NaN.
286
+ ...(bed !== undefined && bedRefused === 0 ? { ambientBed: bed } : {}),
287
+ // Only while on, and only with a narration to level, so the run is otherwise the one it was.
288
+ ...pickLoudness(sources.audio, loudnessOfForm(form.loudness, input.loudness ?? defaultLoudness)),
289
+ // Pauses between sentences: the default for a new run, with a narration to pace.
290
+ ...(sources.audio === "generate" ? pausesOfForm(form) : {}),
188
291
  edgeSilenceSeconds: measure("edgeSilenceSeconds", sources.audio !== "off", defaultEdgeSilenceSeconds, edgeSilenceSecondsProblem),
189
292
  };
190
293
  const parsed = runDraftSchema.safeParse(draft);
@@ -198,3 +301,51 @@ export function toAdmissionDraft(input) {
198
301
  ? { ok: false, fields }
199
302
  : { ok: true, draft: parsed.data };
200
303
  }
304
+ function pickLoudness(audio, loudness) {
305
+ return audio === "off" || loudness === undefined ? {} : { loudness };
306
+ }
307
+ // More images for long videos, from what Play's control holds: the rate as images per hour,
308
+ // and the narration length it is planned for, which is the provided article's own words or
309
+ // else Review's expected words (`images/scale.ts`).
310
+ function imageScaleOf(document, fields) {
311
+ const { form } = document;
312
+ const typed = form.imageScale?.value.trim() ?? "";
313
+ const value = typed === "" ? Number.NaN : Number(typed);
314
+ const minutes = form.imageScale?.every !== "hour";
315
+ const inRange = minutes
316
+ ? value >= everyMinutesMin && value <= everyMinutesMax
317
+ : value >= imagesPerHourMin && value <= imagesPerHourMax;
318
+ if (!inRange)
319
+ fields.push({
320
+ field: "imageScale.value",
321
+ message: minutes
322
+ ? `Enter a number of minutes between ${String(everyMinutesMin)} and ${String(everyMinutesMax)} under Images → More images for long videos.`
323
+ : `Enter a number of images per hour between ${String(imagesPerHourMin)} and ${String(imagesPerHourMax)} under Images → More images for long videos.`,
324
+ });
325
+ const perHour = !inRange
326
+ ? imagesPerHourMin
327
+ : minutes
328
+ ? perHourFromMinutes(value)
329
+ : Math.round(value * 10000) / 10000;
330
+ if (form.sources.article === "provide")
331
+ return { perHour, words: Math.max(1, wordCount(form.provided.article)) };
332
+ // Review checks the expected words on its own (`review-inputs.ts`); a draft whose figure is
333
+ // not a whole number yet plans for Play's starting one.
334
+ const expected = Number(document.expectedWords.trim());
335
+ return {
336
+ perHour,
337
+ words: Number.isInteger(expected) && expected >= 1 && expected <= expectedWordsMax
338
+ ? expected
339
+ : defaultExpectedWords,
340
+ };
341
+ }
342
+ // "Describe tables and figures" and "Skip code" are stored only when on, so a run started
343
+ // with them off saves the config every run made before them did.
344
+ function onlyWhenOn(audio) {
345
+ const { describeFigures, skipCode, ...rest } = audio;
346
+ return {
347
+ ...rest,
348
+ ...(describeFigures === true ? { describeFigures } : {}),
349
+ ...(describeFigures === true && skipCode === true ? { skipCode } : {}),
350
+ };
351
+ }
@@ -2,6 +2,7 @@ import { checkRuntimeModel } from "../../catalog/runtime-models.js";
2
2
  import { modelFields } from "../../catalog/validate.js";
3
3
  import { readinessIsUsable } from "../../kernel/ports/model.js";
4
4
  import { usesNarrationPreparation, usesShorts, usesYoutubeDescription, } from "../admission/rules.js";
5
+ import { usesReviews } from "../reviews/rules.js";
5
6
  import { cliPathChanged } from "../settings/cli-paths.js";
6
7
  import { hasKey, listVoices } from "../settings/repo.js";
7
8
  import { usesAnimation } from "../video/edit-settings.js";
@@ -36,6 +37,8 @@ export function choices(runs) {
36
37
  usesShorts(d) ||
37
38
  usesAnimation(d),
38
39
  },
40
+ // The reviewer is its own text model, checked like the LLM row.
41
+ { field: "reviews", family: "llm", choice: d.reviews, needed: usesReviews(d) },
39
42
  ])
40
43
  .flatMap((c) => (c.needed && c.choice ? [{ ...c, ...c.choice }] : []));
41
44
  return [...new Map(selected.map((c) => [`${c.family}:${c.provider}:${c.model}`, c])).values()];
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { fingerprint } from "../../kernel/runner/work.js";
3
+ import { draftAttachmentKinds } from "./schema.js";
3
4
  const rowSchema = z.object({
4
5
  id: z.string(),
5
6
  schema_version: z.number(),
@@ -21,7 +22,7 @@ const attachmentSchema = z.object({
21
22
  id: z.string(),
22
23
  draft_id: z.string(),
23
24
  staged_file_id: z.string().nullable(),
24
- kind: z.enum(["audio", "images", "thumbnail"]),
25
+ kind: z.enum(draftAttachmentKinds),
25
26
  original_filename: z.string(),
26
27
  status: z.enum(["pending", "ready", "reattach"]),
27
28
  error: z.string().nullable(),
@@ -52,6 +53,13 @@ export function attachmentRefs(document) {
52
53
  ...(p.audio === null ? [] : [{ ...p.audio, kind: "audio" }]),
53
54
  ...p.images.map((file) => ({ ...file, kind: "images" })),
54
55
  ...(p.thumbnail === null ? [] : [{ ...p.thumbnail, kind: "thumbnail" }]),
56
+ ...(p.reference === undefined || p.reference === null
57
+ ? []
58
+ : [{ ...p.reference, kind: "reference" }]),
59
+ // The shorts' music is staged like narration audio, so it is an audio attachment.
60
+ ...(p.shortsMusic == null ? [] : [{ ...p.shortsMusic, kind: "audio" }]),
61
+ // So is the ambient bed's own file.
62
+ ...(p.ambientBed == null ? [] : [{ ...p.ambientBed, kind: "audio" }]),
55
63
  ];
56
64
  }
57
65
  export function requestHash(value) {
@@ -3,6 +3,7 @@ import { readFileSync, statSync } from "node:fs";
3
3
  import { z } from "zod";
4
4
  import { modelFields } from "../../catalog/validate.js";
5
5
  import { admit } from "../admission/rules.js";
6
+ import { brandedForm, brandedRun, castSnapshot, castVoicedRun, draftChannel, } from "../channels/runs.js";
6
7
  import { estimateRun } from "../estimate/index.js";
7
8
  import { listEntries } from "../library/repo.js";
8
9
  import { pickTemplates, renderPicked } from "../library/slots.js";
@@ -51,12 +52,27 @@ export function resolveReviewInputs(deps, view, font) {
51
52
  field: "variants",
52
53
  message: "You can review at most 50 videos at once, counting the first one. Remove some variations.",
53
54
  });
54
- const converted = toAdmissionDraft({
55
- document,
55
+ const entries = [...(document.librarySnapshot?.entries ?? []), ...listEntries(deps.db)];
56
+ // The channel's brand kit fills what the setup leaves at its default, and its cast rides
57
+ // along for the images (`slices/channels/runs.ts`).
58
+ const channel = draftChannel(deps.db, document);
59
+ const useBrandKit = document.form.useBrandKit !== false;
60
+ const branded = toAdmissionDraft({
61
+ document: {
62
+ ...document,
63
+ form: brandedForm(deps.db, document.form, channel.brand, entries),
64
+ },
56
65
  attachments: fresh.value.attachments,
57
- entries: [...(document.librarySnapshot?.entries ?? []), ...listEntries(deps.db)],
66
+ entries,
58
67
  silenceGapSeconds: readSettings(deps).silenceGapSeconds,
68
+ loudness: readSettings(deps).loudness,
59
69
  });
70
+ const converted = branded.ok
71
+ ? {
72
+ ok: true,
73
+ draft: castVoicedRun(deps.db, brandedRun(branded.draft, channel, castSnapshot(deps.db, channel.id), useBrandKit)),
74
+ }
75
+ : branded;
60
76
  if (!converted.ok)
61
77
  fields.push(...converted.fields);
62
78
  if (!converted.ok || !words.success || fields.length)
@@ -110,7 +126,10 @@ export function resolveReviewInputs(deps, view, font) {
110
126
  const activeIds = new Set(runs.flatMap((run) => [
111
127
  run.draft.provided.audio,
112
128
  run.draft.provided.thumbnail,
129
+ run.draft.provided.reference,
113
130
  ...(run.draft.provided.images ?? []),
131
+ run.draft.provided.shortsMusic,
132
+ run.draft.provided.ambientBed,
114
133
  ]));
115
134
  const attachmentIdentity = fresh.value.attachments.flatMap((file) => file.stagedFileId !== null && activeIds.has(file.stagedFileId)
116
135
  ? [{ id: file.id, stagedFileId: file.stagedFileId, bytes: file.bytes }]
@@ -1,20 +1,26 @@
1
1
  import { z } from "zod";
2
2
  import { formats, stageKinds } from "../../kernel/pipeline.js";
3
+ import { languageSchema } from "../../kernel/ports/languages.js";
3
4
  import { thinkingModes } from "../../kernel/ports/llm.js";
4
- import { motionStyles, stageSources } from "../admission/model.js";
5
+ import { motionStyles, referenceSources, stageSources } from "../admission/model.js";
5
6
  import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultMotionStyle, defaultZoomPercent, } from "../admission/rules.js";
6
7
  import { runDraftSchema, videoEditSchema } from "../admission/schema.js";
7
8
  import { checkpointRowSchema, checkpointStageSchema } from "../checkpoints/schema.js";
8
9
  import { documentSettingsSchema } from "../document/theme-schema.js";
9
10
  import { librarySnapshotSchema } from "../library/snapshot.js";
10
11
  import { chunkModes } from "../narration/chunk.js";
12
+ import { reviewModes, reviewStages } from "../reviews/model.js";
11
13
  import { subtitleModes, subtitlePositions } from "../subtitles/model.js";
14
+ import { ambientBedSources } from "../video/ambient-bed.js";
15
+ import { voicesSettingsSchema } from "../voices/model.js";
12
16
  const id = z.uuid();
13
17
  const text = z.string();
14
18
  const values = z.record(text, text).readonly();
15
19
  const provider = z
16
20
  .object({ provider: text, model: text, thinking: z.enum(thinkingModes).optional() })
17
21
  .strict();
22
+ // What a draft's uploads are for: a stage's files, or the Images stage's establishing image.
23
+ export const draftAttachmentKinds = ["audio", "images", "thumbnail", "reference"];
18
24
  const file = z.object({ attachmentId: id, name: text }).strict().readonly();
19
25
  export const playDraftFormSchema = z
20
26
  .object({
@@ -42,14 +48,52 @@ export const playDraftFormSchema = z
42
48
  voice: text,
43
49
  usePronunciationGlossary: z.boolean().optional(),
44
50
  shareGlossary: z.boolean().optional(),
51
+ useNarrationAliases: z.boolean().optional(),
52
+ describeFigures: z.boolean().optional(),
53
+ skipCode: z.boolean().optional(),
45
54
  })
46
55
  .readonly(),
47
56
  images: provider.readonly(),
57
+ // Absent on drafts and templates saved before the establishing image: Off. The prompt is
58
+ // an image prompt's name, "" when none is picked.
59
+ reference: z
60
+ .object({
61
+ source: z.enum(["off", ...referenceSources]),
62
+ prompt: text,
63
+ thumbnail: z.boolean(),
64
+ })
65
+ .strict()
66
+ .readonly()
67
+ .optional(),
48
68
  articlePrompt: text,
49
69
  narrationPrompt: text.optional(),
70
+ // Absent on drafts and templates saved before three thumbnails: one.
71
+ thumbnailCount: z.union([z.literal(1), z.literal(3)]).optional(),
72
+ // Level the volume (`loudness/model.ts`). Absent on drafts and templates nobody changed it
73
+ // in: Settings → General's default, which is on for a new run. The targets are LUFS; absent
74
+ // is Settings' own.
75
+ loudness: z
76
+ .object({
77
+ enabled: z.boolean(),
78
+ videoLufs: z.number().optional(),
79
+ audioFilesLufs: z.number().optional(),
80
+ })
81
+ .strict()
82
+ .readonly()
83
+ .optional(),
84
+ // Pauses between sentences and paragraphs in seconds, raw text like every number on Play.
85
+ // Absent on drafts and templates nobody changed them in: the sentence default and no
86
+ // paragraph minimum (`narration/pauses-model.ts`).
87
+ sentencePause: text.optional(),
88
+ paragraphPause: text.optional(),
50
89
  // Absent on drafts and templates saved before the YouTube description: off, built-in prompt.
51
90
  youtubeDescription: z.boolean().optional(),
91
+ showFigures: z.boolean().optional(),
52
92
  descriptionPrompt: text.optional(),
93
+ // The project language (`kernel/ports/languages.ts`). Absent on drafts and templates saved
94
+ // before it, and on a draft nobody picked one for: the channel's language, else English.
95
+ // English picked on purpose is kept as "en", so a channel's language does not replace it.
96
+ language: languageSchema.optional(),
53
97
  // Absent on drafts and templates saved before Shorts: off. The numbers are raw text, like
54
98
  // every other number on Play; the prompts are names, "" being the built-in ones.
55
99
  shorts: z
@@ -70,7 +114,33 @@ export const playDraftFormSchema = z
70
114
  .strict()
71
115
  .readonly()
72
116
  .optional(),
117
+ // Absent on drafts and templates saved before automatic reviews: every review Off. The
118
+ // retries are raw text like every other number on Play; a stage's prompt is a Review
119
+ // prompt's name, "" being the built-in one.
120
+ reviews: z
121
+ .object({
122
+ provider: text,
123
+ model: text,
124
+ thinking: z.enum(thinkingModes).optional(),
125
+ retries: text,
126
+ stages: z
127
+ .partialRecord(z.enum(reviewStages), z
128
+ .object({ mode: z.enum(reviewModes), prompt: text })
129
+ .strict()
130
+ .readonly())
131
+ .readonly(),
132
+ })
133
+ .strict()
134
+ .readonly()
135
+ .optional(),
73
136
  imagePrompts: z.array(z.object({ name: text, number: text }).strict().readonly()).readonly(),
137
+ // More images for long videos, as typed: "every [value] minutes" or "[value] per hour".
138
+ // Absent is off, which is what every draft and template saved before it was.
139
+ imageScale: z
140
+ .object({ every: z.enum(["minutes", "hour"]), value: text })
141
+ .strict()
142
+ .readonly()
143
+ .optional(),
74
144
  thumbnailPrompt: text,
75
145
  intro: text,
76
146
  outro: text,
@@ -94,9 +164,30 @@ export const playDraftFormSchema = z
94
164
  edgeSilenceSeconds: text.default(String(defaultEdgeSilenceSeconds)),
95
165
  zoomPercent: text.default(String(defaultZoomPercent)),
96
166
  motionStyle: z.enum(motionStyles).default(defaultMotionStyle),
167
+ // The silence between narration segments for this run, raw text like the numbers above.
168
+ // Absent or empty: the gap Settings has.
169
+ silenceGapSeconds: text.optional(),
97
170
  // Absent on drafts and templates saved before the edit settings: today's slideshow. Every
98
171
  // field is a pick from a list, so it is kept as the settings themselves.
99
172
  videoEdit: videoEditSchema.strict().readonly().optional(),
173
+ // Whether the channel's brand kit fills what this setup leaves at its default
174
+ // (`slices/channels/runs.ts`). Absent is on.
175
+ useBrandKit: z.boolean().optional(),
176
+ // Absent on drafts and templates saved before multiple voices: the Narration format. Every
177
+ // number is a pick from a list, so it is kept as the settings themselves.
178
+ voices: voicesSettingsSchema.strict().readonly().optional(),
179
+ // The ambient bed under the long video (`video/ambient-bed.ts`), numbers as typed. Absent
180
+ // takes the channel's brand kit's, if any; "none" asks for no bed whatever the channel has.
181
+ ambientBed: z
182
+ .object({
183
+ source: z.enum(["none", ...ambientBedSources]),
184
+ level: text,
185
+ fadeIn: text,
186
+ tail: text,
187
+ })
188
+ .strict()
189
+ .readonly()
190
+ .optional(),
100
191
  values,
101
192
  provided: z
102
193
  .object({
@@ -105,6 +196,14 @@ export const playDraftFormSchema = z
105
196
  audio: file.nullable(),
106
197
  thumbnail: file.nullable(),
107
198
  images: z.array(file).readonly(),
199
+ // The uploaded establishing image; absent on drafts saved before it.
200
+ reference: file.nullable().optional(),
201
+ // The Shorts step's background music, uploaded as an audio attachment. Absent on
202
+ // drafts and templates saved before Play offered it: no music.
203
+ shortsMusic: file.nullable().optional(),
204
+ // The ambient bed's own file, an audio attachment; used only while its source is
205
+ // "upload". Absent on drafts and templates saved before it.
206
+ ambientBed: file.nullable().optional(),
108
207
  })
109
208
  .strict()
110
209
  .readonly(),
@@ -120,6 +219,8 @@ export const playDraftDocumentSchema = z
120
219
  .strict()
121
220
  .readonly()
122
221
  .optional(),
222
+ // The channel picked on Play. Absent runs in the template's channel, or the default one.
223
+ channelId: id.optional(),
123
224
  form: playDraftFormSchema,
124
225
  section: z.enum(["content", "outputs", "style", "review"]),
125
226
  variants: z.array(z.object({ id, title: text, values }).strict().readonly()).readonly(),
@@ -150,7 +251,7 @@ export const discardDraftInputSchema = z
150
251
  export const draftAttachmentSchema = z
151
252
  .object({
152
253
  id,
153
- kind: z.enum(["audio", "images", "thumbnail"]),
254
+ kind: z.enum(draftAttachmentKinds),
154
255
  name: text,
155
256
  state: z.enum(["pending", "copying", "ready", "reattach"]),
156
257
  stagedFileId: text.nullable(),
@@ -202,6 +303,9 @@ const costEstimateSchema = z
202
303
  low: z.number().nullable(),
203
304
  high: z.number().nullable(),
204
305
  detail: text,
306
+ onPlan: z.boolean().optional(),
307
+ apiLow: z.number().nullable().optional(),
308
+ apiHigh: z.number().nullable().optional(),
205
309
  })
206
310
  .strict()
207
311
  .readonly())
@@ -209,6 +313,9 @@ const costEstimateSchema = z
209
313
  low: z.number(),
210
314
  high: z.number(),
211
315
  unknown: z.number(),
316
+ apiLow: z.number().optional(),
317
+ apiHigh: z.number().optional(),
318
+ apiUnknown: z.number().optional(),
212
319
  expectedWords: z.number(),
213
320
  catalogueDate: text.nullable(),
214
321
  assumptions: z.array(text).readonly(),
@@ -44,7 +44,7 @@ function parseStoredJson(value) {
44
44
  throw error;
45
45
  }
46
46
  }
47
- function documentOf(row) {
47
+ export function documentOf(row) {
48
48
  if (row.schema_version !== 1)
49
49
  return null;
50
50
  const parsed = playDraftDocumentSchema.safeParse(parseStoredJson(row.document_json));
@@ -208,6 +208,15 @@ export function forkDraft(deps, input) {
208
208
  audio: p.audio === null ? null : rewrite(p.audio),
209
209
  thumbnail: p.thumbnail === null ? null : rewrite(p.thumbnail),
210
210
  images: p.images.map(rewrite),
211
+ ...(p.reference === undefined
212
+ ? {}
213
+ : { reference: p.reference === null ? null : rewrite(p.reference) }),
214
+ ...(p.shortsMusic === undefined
215
+ ? {}
216
+ : { shortsMusic: p.shortsMusic === null ? null : rewrite(p.shortsMusic) }),
217
+ ...(p.ambientBed === undefined
218
+ ? {}
219
+ : { ambientBed: p.ambientBed === null ? null : rewrite(p.ambientBed) }),
211
220
  },
212
221
  },
213
222
  };
@@ -80,7 +80,47 @@ export async function startPlayDraft(deps, input) {
80
80
  };
81
81
  const checked = await checkDraftReadiness(captured, claimed.value.runs);
82
82
  const oldFiles = attachmentRows(deps.db, input.draftId).flatMap((a) => a.staged_file_id === null ? [] : [a.staged_file_id]);
83
- const result = transact(deps.db, () => {
83
+ let result;
84
+ try {
85
+ result = createReviewedRuns(deps, input, captured, checked, catalogue);
86
+ }
87
+ catch (error) {
88
+ if (!(error instanceof StartedRunMismatch))
89
+ throw error;
90
+ // Nothing the review promised was created: the transaction rolled back.
91
+ releaseStartClaim(deps, input, true);
92
+ try {
93
+ deps.log.write("error", "play.start.run-count", { detail: error.message });
94
+ }
95
+ catch {
96
+ /* The refusal below still reaches the person. */
97
+ }
98
+ return { ok: false, reason: "stale-review", currentVersion: input.baseVersion, fields: [] };
99
+ }
100
+ if (!result.ok) {
101
+ releaseStartClaim(deps, input, result.reason === "stale-review");
102
+ return result;
103
+ }
104
+ if (!result.value.replayed) {
105
+ afterCommit(deps, () => deps.recordStarted(result.value.projectIds));
106
+ for (const id of oldFiles)
107
+ afterCommit(deps, () => releaseStagedFile(deps, id));
108
+ afterCommit(deps, () => {
109
+ if (result.value.queue.length)
110
+ pumpQueue(deps.db, deps.runner);
111
+ else
112
+ for (const id of result.value.projectIds)
113
+ deps.runner.tick(id);
114
+ });
115
+ }
116
+ return result;
117
+ }
118
+ // Start creates exactly the runs the person reviewed: one project for one run, otherwise a
119
+ // queue entry and project per run. Anything else is thrown so the transaction rolls back.
120
+ class StartedRunMismatch extends Error {
121
+ }
122
+ function createReviewedRuns(deps, input, captured, checked, catalogue) {
123
+ return transact(deps.db, () => {
84
124
  const receipt = readStartReceipt(deps.db, input.reviewId);
85
125
  if (receipt)
86
126
  return replayResult(receipt, input);
@@ -109,27 +149,12 @@ export async function startPlayDraft(deps, input) {
109
149
  replayed: false,
110
150
  ...(checkpointSet.length ? { checkpointSet } : {}),
111
151
  };
152
+ if (projectIds.length !== runs.length || queue.length !== (runs.length === 1 ? 0 : runs.length))
153
+ throw new StartedRunMismatch(`Start would create ${projectIds.length} of ${runs.length} reviewed videos (${queue.length} queued).`);
112
154
  insertStartReceipt(deps, input, value);
113
155
  markDraftStartedAndReleaseRefs(deps.db, input);
114
156
  return { ok: true, value };
115
157
  });
116
- if (!result.ok) {
117
- releaseStartClaim(deps, input, result.reason === "stale-review");
118
- return result;
119
- }
120
- if (!result.value.replayed) {
121
- afterCommit(deps, () => deps.recordStarted(result.value.projectIds));
122
- for (const id of oldFiles)
123
- afterCommit(deps, () => releaseStagedFile(deps, id));
124
- afterCommit(deps, () => {
125
- if (result.value.queue.length)
126
- pumpQueue(deps.db, deps.runner);
127
- else
128
- for (const id of result.value.projectIds)
129
- deps.runner.tick(id);
130
- });
131
- }
132
- return result;
133
158
  }
134
159
  function afterCommit(deps, run) {
135
160
  try {
@@ -29,7 +29,8 @@ export async function uploadDraftAttachment(deps, input) {
29
29
  return refuse("conflict");
30
30
  const storage = { ...deps, emit: () => undefined };
31
31
  const result = await stageUpload(storage, {
32
- stageKind: attachment.kind,
32
+ // The establishing image is the Images stage's upload.
33
+ stageKind: attachment.kind === "reference" ? "images" : attachment.kind,
33
34
  originalFilename: attachment.original_filename,
34
35
  content: input.content,
35
36
  onAllocated(file) {