@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -2,8 +2,14 @@ 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";
7
13
  // Said where the control is, since the music sits in a closed disclosure on the Export rail.
8
14
  const musicMessages = {
9
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.",
@@ -12,6 +18,14 @@ const musicMessages = {
12
18
  function pickMusic(id) {
13
19
  return id === undefined ? {} : { shortsMusic: id };
14
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
+ }
15
29
  export function toAdmissionDraft(input) {
16
30
  const { form } = input.document;
17
31
  const fields = [];
@@ -108,7 +122,28 @@ export function toAdmissionDraft(input) {
108
122
  : defaultShorts.maxSeconds,
109
123
  };
110
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;
111
145
  const draft = {
146
+ ...(reviews === undefined ? {} : { reviews }),
112
147
  ...(form.checkpoints === undefined ? {} : { checkpoints: form.checkpoints }),
113
148
  title: form.title,
114
149
  format: form.format,
@@ -123,11 +158,12 @@ export function toAdmissionDraft(input) {
123
158
  : {}),
124
159
  llm: form.llm,
125
160
  audio: sources.audio === "generate" || form.audio.usePronunciationGlossary !== undefined
126
- ? // With the glossary on, sharing it is on unless the draft turned it off, as Play
127
- // shows it for a draft saved before the switch existed.
128
- form.audio.usePronunciationGlossary === true && form.audio.shareGlossary === undefined
129
- ? { ...form.audio, shareGlossary: true }
130
- : 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)
131
167
  : undefined,
132
168
  images: form.images,
133
169
  // The establishing image belongs to generated images; Off, or images not generated,
@@ -143,10 +179,25 @@ export function toAdmissionDraft(input) {
143
179
  },
144
180
  }
145
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
+ : {}),
146
188
  articlePrompt: sources.article === "generate" ? form.articlePrompt : undefined,
147
189
  ...(sources.audio === "generate" && form.narrationPrompt?.trim()
148
190
  ? { narrationPrompt: form.narrationPrompt }
149
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
+ : {}),
150
201
  // Timed from the narration, so a switch left on with narration Off asks for nothing.
151
202
  ...(sources.audio !== "off" && form.youtubeDescription === true
152
203
  ? {
@@ -163,6 +214,9 @@ export function toAdmissionDraft(input) {
163
214
  number: number(prompt.number, `imagePrompts.${index}.number`, numberPerPromptMax),
164
215
  }))
165
216
  : [],
217
+ ...(sources.images === "generate" && form.imageScale !== undefined
218
+ ? { imageScale: imageScaleOf(input.document, fields) }
219
+ : {}),
166
220
  thumbnailPrompt: ["from_prompt", "prompt_by_llm"].includes(sources.thumbnail)
167
221
  ? form.thumbnailPrompt
168
222
  : undefined,
@@ -192,6 +246,10 @@ export function toAdmissionDraft(input) {
192
246
  ...(shortsOn && form.provided.shortsMusic
193
247
  ? pickMusic(file(form.provided.shortsMusic, "audio", "shorts.music", musicMessages))
194
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
+ : {}),
195
253
  },
196
254
  chunking: {
197
255
  mode: chunkMode,
@@ -209,7 +267,11 @@ export function toAdmissionDraft(input) {
209
267
  mode,
210
268
  fontSize: number(form.subtitles.fontSize, "subtitles.fontSize", 120, 16),
211
269
  },
212
- 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),
213
275
  imageSeconds: measure("imageSeconds", sources.video === "generate" || shortsOn, defaultImageSeconds, imageSecondsProblem),
214
276
  zoomPercent: measure("zoomPercent", sources.video === "generate", defaultZoomPercent, zoomPercentProblem),
215
277
  // A choice from a list, so there is nothing to refuse; a run without a video keeps it
@@ -217,6 +279,15 @@ export function toAdmissionDraft(input) {
217
279
  motionStyle: form.motionStyle,
218
280
  // Kept the same way; admission checks it only while the video renders.
219
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) : {}),
220
291
  edgeSilenceSeconds: measure("edgeSilenceSeconds", sources.audio !== "off", defaultEdgeSilenceSeconds, edgeSilenceSecondsProblem),
221
292
  };
222
293
  const parsed = runDraftSchema.safeParse(draft);
@@ -230,3 +301,51 @@ export function toAdmissionDraft(input) {
230
301
  ? { ok: false, fields }
231
302
  : { ok: true, draft: parsed.data };
232
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()];
@@ -58,6 +58,8 @@ export function attachmentRefs(document) {
58
58
  : [{ ...p.reference, kind: "reference" }]),
59
59
  // The shorts' music is staged like narration audio, so it is an audio attachment.
60
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" }]),
61
63
  ];
62
64
  }
63
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)
@@ -113,6 +129,7 @@ export function resolveReviewInputs(deps, view, font) {
113
129
  run.draft.provided.reference,
114
130
  ...(run.draft.provided.images ?? []),
115
131
  run.draft.provided.shortsMusic,
132
+ run.draft.provided.ambientBed,
116
133
  ]));
117
134
  const attachmentIdentity = fresh.value.attachments.flatMap((file) => file.stagedFileId !== null && activeIds.has(file.stagedFileId)
118
135
  ? [{ id: file.id, stagedFileId: file.stagedFileId, bytes: file.bytes }]
@@ -1,5 +1,6 @@
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
5
  import { motionStyles, referenceSources, stageSources } from "../admission/model.js";
5
6
  import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultMotionStyle, defaultZoomPercent, } from "../admission/rules.js";
@@ -8,7 +9,10 @@ import { checkpointRowSchema, checkpointStageSchema } from "../checkpoints/schem
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();
@@ -44,6 +48,9 @@ export const playDraftFormSchema = z
44
48
  voice: text,
45
49
  usePronunciationGlossary: z.boolean().optional(),
46
50
  shareGlossary: z.boolean().optional(),
51
+ useNarrationAliases: z.boolean().optional(),
52
+ describeFigures: z.boolean().optional(),
53
+ skipCode: z.boolean().optional(),
47
54
  })
48
55
  .readonly(),
49
56
  images: provider.readonly(),
@@ -60,9 +67,33 @@ export const playDraftFormSchema = z
60
67
  .optional(),
61
68
  articlePrompt: text,
62
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(),
63
89
  // Absent on drafts and templates saved before the YouTube description: off, built-in prompt.
64
90
  youtubeDescription: z.boolean().optional(),
91
+ showFigures: z.boolean().optional(),
65
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(),
66
97
  // Absent on drafts and templates saved before Shorts: off. The numbers are raw text, like
67
98
  // every other number on Play; the prompts are names, "" being the built-in ones.
68
99
  shorts: z
@@ -83,7 +114,33 @@ export const playDraftFormSchema = z
83
114
  .strict()
84
115
  .readonly()
85
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(),
86
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(),
87
144
  thumbnailPrompt: text,
88
145
  intro: text,
89
146
  outro: text,
@@ -107,9 +164,30 @@ export const playDraftFormSchema = z
107
164
  edgeSilenceSeconds: text.default(String(defaultEdgeSilenceSeconds)),
108
165
  zoomPercent: text.default(String(defaultZoomPercent)),
109
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(),
110
170
  // Absent on drafts and templates saved before the edit settings: today's slideshow. Every
111
171
  // field is a pick from a list, so it is kept as the settings themselves.
112
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(),
113
191
  values,
114
192
  provided: z
115
193
  .object({
@@ -123,6 +201,9 @@ export const playDraftFormSchema = z
123
201
  // The Shorts step's background music, uploaded as an audio attachment. Absent on
124
202
  // drafts and templates saved before Play offered it: no music.
125
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(),
126
207
  })
127
208
  .strict()
128
209
  .readonly(),
@@ -138,6 +219,8 @@ export const playDraftDocumentSchema = z
138
219
  .strict()
139
220
  .readonly()
140
221
  .optional(),
222
+ // The channel picked on Play. Absent runs in the template's channel, or the default one.
223
+ channelId: id.optional(),
141
224
  form: playDraftFormSchema,
142
225
  section: z.enum(["content", "outputs", "style", "review"]),
143
226
  variants: z.array(z.object({ id, title: text, values }).strict().readonly()).readonly(),
@@ -220,6 +303,9 @@ const costEstimateSchema = z
220
303
  low: z.number().nullable(),
221
304
  high: z.number().nullable(),
222
305
  detail: text,
306
+ onPlan: z.boolean().optional(),
307
+ apiLow: z.number().nullable().optional(),
308
+ apiHigh: z.number().nullable().optional(),
223
309
  })
224
310
  .strict()
225
311
  .readonly())
@@ -227,6 +313,9 @@ const costEstimateSchema = z
227
313
  low: z.number(),
228
314
  high: z.number(),
229
315
  unknown: z.number(),
316
+ apiLow: z.number().optional(),
317
+ apiHigh: z.number().optional(),
318
+ apiUnknown: z.number().optional(),
230
319
  expectedWords: z.number(),
231
320
  catalogueDate: text.nullable(),
232
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));
@@ -214,6 +214,9 @@ export function forkDraft(deps, input) {
214
214
  ...(p.shortsMusic === undefined
215
215
  ? {}
216
216
  : { shortsMusic: p.shortsMusic === null ? null : rewrite(p.shortsMusic) }),
217
+ ...(p.ambientBed === undefined
218
+ ? {}
219
+ : { ambientBed: p.ambientBed === null ? null : rewrite(p.ambientBed) }),
217
220
  },
218
221
  },
219
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 {
@@ -3,11 +3,16 @@ import { transact } from "../../kernel/db/tx.js";
3
3
  import { sourceOf } from "../admission/model.js";
4
4
  import { projectById } from "../admission/repo.js";
5
5
  import { detectSlots } from "../admission/substitute.js";
6
+ import { projectChannelId } from "../channels/repo.js";
6
7
  import { listCheckpoints } from "../checkpoints/repo.js";
7
8
  import { documentThemeOf } from "../document/model.js";
9
+ import { imageScaleForm } from "../images/scale.js";
8
10
  import { requestHash } from "../play-drafts/repo.js";
11
+ import { reviewSettingsForm } from "../reviews/model.js";
9
12
  import { currentRevisionId, revisionById } from "../revisions/repo.js";
10
13
  import { shortsExtrasForm } from "../shorts/model.js";
14
+ import { ambientBedFormOf } from "../video/ambient-bed.js";
15
+ import { usesScriptPrompt } from "../voices/model.js";
11
16
  import { createTemplate, readTemplate } from "./service.js";
12
17
  export const projectTemplateCreateSchema = z
13
18
  .object({
@@ -47,7 +52,11 @@ export function createTemplateFromProject(deps, input) {
47
52
  const result = createTemplate(deps, {
48
53
  id: parsed.data.id,
49
54
  name: parsed.data.name,
50
- document: documentFromProject(deps, revision),
55
+ // The project's channel, so the template lands where the video came from.
56
+ document: {
57
+ ...documentFromProject(deps, revision),
58
+ channelId: projectChannelId(deps.db, project.id),
59
+ },
51
60
  });
52
61
  if (result.ok)
53
62
  deps.db
@@ -75,14 +84,21 @@ function documentFromProject(deps, revision) {
75
84
  updatedAt: revision.createdAt,
76
85
  });
77
86
  };
78
- addPrompt("article", config.articlePrompt, "article");
87
+ addPrompt(usesScriptPrompt(config) ? "script" : "article", config.articlePrompt, "article");
79
88
  addPrompt("narration", config.narrationPrompt, "narration");
80
89
  addPrompt("description", config.descriptionPrompt, "description");
90
+ for (const [stage, picked] of Object.entries(config.reviews?.stages ?? {}))
91
+ addPrompt("review", picked.prompt, `review.${stage}`);
81
92
  const definitions = revision.content.imageOrder.flatMap((key) => {
82
93
  const definition = revision.content.imageDefinitions[key];
83
94
  return definition === undefined ? [] : [definition];
84
95
  });
85
- const imagePrompts = config.sources.images === "generate" && definitions.length > 0
96
+ const imagePrompts =
97
+ // A project that scales its images with the narration's length keeps its ticked prompts
98
+ // and the setting, rather than one prompt per image of the count it planned.
99
+ config.sources.images === "generate" &&
100
+ definitions.length > 0 &&
101
+ config.imageScale === undefined
86
102
  ? definitions.map((definition, index) => {
87
103
  const key = definition.templateKey ?? "";
88
104
  const original = revision.content.promptTemplates[key];
@@ -139,6 +155,9 @@ function documentFromProject(deps, revision) {
139
155
  ...(config.shorts?.enabled === true && revision.content.shortsMusic !== undefined
140
156
  ? { shortsMusic: attachment("Music from project") }
141
157
  : {}),
158
+ ...(config.ambientBed?.source === "upload" && revision.content.ambientBed !== undefined
159
+ ? { ambientBed: attachment("Ambient sound from project") }
160
+ : {}),
142
161
  };
143
162
  return {
144
163
  schemaVersion: 1,
@@ -168,8 +187,10 @@ function documentFromProject(deps, revision) {
168
187
  },
169
188
  }),
170
189
  articlePrompt: config.articlePrompt ?? "",
190
+ ...(config.thumbnailCount === 3 ? { thumbnailCount: 3 } : {}),
171
191
  ...(config.narrationPrompt === undefined ? {} : { narrationPrompt: config.narrationPrompt }),
172
192
  ...(config.youtubeDescription === true ? { youtubeDescription: true } : {}),
193
+ ...(config.language === undefined ? {} : { language: config.language }),
173
194
  ...(config.descriptionPrompt === undefined
174
195
  ? {}
175
196
  : { descriptionPrompt: config.descriptionPrompt }),
@@ -189,7 +210,31 @@ function documentFromProject(deps, revision) {
189
210
  // A project without edit settings makes a template without them, so a video made from
190
211
  // it cuts every N seconds like the project did.
191
212
  ...(config.videoEdit === undefined ? {} : { videoEdit: config.videoEdit }),
213
+ ...(config.reviews === undefined ? {} : { reviews: reviewSettingsForm(config.reviews) }),
214
+ ...(config.voices === undefined ? {} : { voices: config.voices }),
215
+ ...(config.ambientBed === undefined
216
+ ? {}
217
+ : { ambientBed: ambientBedFormOf(config.ambientBed) }),
218
+ // Level the volume at the project's targets while it is on; off leaves it to Settings'
219
+ // default, which is how a template made before the setting starts a run.
220
+ ...(config.loudness === undefined
221
+ ? {}
222
+ : {
223
+ loudness: {
224
+ enabled: true,
225
+ videoLufs: config.loudness.videoLufs,
226
+ audioFilesLufs: config.loudness.audioFilesLufs,
227
+ },
228
+ }),
229
+ // The project's pauses; a project without them leaves the template to the defaults.
230
+ ...(config.sentencePauseSeconds === undefined
231
+ ? {}
232
+ : { sentencePause: String(config.sentencePauseSeconds) }),
233
+ ...(config.paragraphPauseSeconds === undefined
234
+ ? {}
235
+ : { paragraphPause: String(config.paragraphPauseSeconds) }),
192
236
  imagePrompts,
237
+ ...(config.imageScale === undefined ? {} : { imageScale: imageScaleForm(config.imageScale) }),
193
238
  thumbnailPrompt: config.thumbnailPrompt ?? "",
194
239
  intro: config.intro?.name ?? "",
195
240
  outro: config.outro?.name ?? "",
@@ -214,7 +259,8 @@ function documentFromProject(deps, revision) {
214
259
  },
215
260
  section: "content",
216
261
  variants: [],
217
- expectedWords: "1500",
262
+ // The length a project that scales its images planned for, so the template plans the same.
263
+ expectedWords: String(config.imageScale?.words ?? 1500),
218
264
  previewText: "Every story begins with a word.",
219
265
  fontUpload: null,
220
266
  };