@gentbajko/slopify 2.5.0 → 3.0.1

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 (542) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +24 -41
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +31 -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 +85 -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/gemini.js +206 -0
  44. package/dist/adapters/tts/inworld-async.js +9 -5
  45. package/dist/adapters/tts/inworld.js +17 -3
  46. package/dist/adapters/tts/openai.js +3 -0
  47. package/dist/adapters/tts/pcm-mp3.js +62 -0
  48. package/dist/adapters/tts/system.js +147 -0
  49. package/dist/adapters/tts/voice-languages.js +80 -0
  50. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  55. package/dist/assets/fonts/noto/OFL.txt +93 -0
  56. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  57. package/dist/assets/models.yaml +38 -0
  58. package/dist/assets/sample/sample-audiobook.tar +0 -0
  59. package/dist/assets/sample/sample-podcast.tar +0 -0
  60. package/dist/assets/sample/sample-project.tar +0 -0
  61. package/dist/assets/style-preview/narration.mp3 +0 -0
  62. package/dist/assets/style-preview/tall-1.jpg +0 -0
  63. package/dist/assets/style-preview/tall-2.jpg +0 -0
  64. package/dist/assets/style-preview/tall-3.jpg +0 -0
  65. package/dist/assets/style-preview/wide-1.jpg +0 -0
  66. package/dist/assets/style-preview/wide-2.jpg +0 -0
  67. package/dist/assets/style-preview/wide-3.jpg +0 -0
  68. package/dist/catalog/merge.js +132 -0
  69. package/dist/catalog/registry.js +17 -14
  70. package/dist/catalog/schema.js +2 -0
  71. package/dist/catalog/store.js +147 -8
  72. package/dist/catalog/validate.js +2 -2
  73. package/dist/compose.yaml +71 -0
  74. package/dist/edge/autostart/docker-record.js +32 -0
  75. package/dist/edge/autostart/docker.js +118 -0
  76. package/dist/edge/autostart/index.js +78 -0
  77. package/dist/edge/autostart/launcher.js +176 -0
  78. package/dist/edge/autostart/model.js +3 -0
  79. package/dist/edge/autostart/native.js +210 -0
  80. package/dist/edge/autostart/prompt.js +72 -0
  81. package/dist/edge/autostart/service.js +90 -0
  82. package/dist/edge/cli-args.js +71 -0
  83. package/dist/edge/cli.js +64 -63
  84. package/dist/edge/docker-install/activation.js +56 -0
  85. package/dist/edge/docker-install/apply.js +458 -0
  86. package/dist/edge/docker-install/engine.js +362 -0
  87. package/dist/edge/docker-install/run.js +160 -0
  88. package/dist/edge/docker-install/state.js +146 -0
  89. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  90. package/dist/edge/docker.js +14 -22
  91. package/dist/edge/events/hub.js +3 -1
  92. package/dist/edge/host-cli.js +1 -1
  93. package/dist/edge/http/actions.js +38 -11
  94. package/dist/edge/http/app.js +60 -2
  95. package/dist/edge/http/auditions.js +70 -0
  96. package/dist/edge/http/autostart.js +37 -0
  97. package/dist/edge/http/byte-range.js +42 -0
  98. package/dist/edge/http/channel-memory.js +92 -0
  99. package/dist/edge/http/channels.js +128 -0
  100. package/dist/edge/http/diagnostics.js +1 -0
  101. package/dist/edge/http/draft-files.js +2 -4
  102. package/dist/edge/http/entries.js +22 -3
  103. package/dist/edge/http/files.js +11 -7
  104. package/dist/edge/http/folder-location.js +1 -1
  105. package/dist/edge/http/home.js +39 -0
  106. package/dist/edge/http/host-cli.js +29 -5
  107. package/dist/edge/http/narration-peaks.js +76 -0
  108. package/dist/edge/http/onboarding.js +212 -0
  109. package/dist/edge/http/open-folder.js +1 -1
  110. package/dist/edge/http/patch-notes.js +45 -0
  111. package/dist/edge/http/planning.js +4 -2
  112. package/dist/edge/http/project-create.js +87 -0
  113. package/dist/edge/http/project-templates.js +19 -2
  114. package/dist/edge/http/projects.js +37 -100
  115. package/dist/edge/http/prompts.js +40 -3
  116. package/dist/edge/http/pronunciations.js +23 -4
  117. package/dist/edge/http/providers.js +119 -3
  118. package/dist/edge/http/reviews.js +131 -0
  119. package/dist/edge/http/revision-files.js +3 -5
  120. package/dist/edge/http/revisions.js +1 -1
  121. package/dist/edge/http/run-cost.js +25 -0
  122. package/dist/edge/http/schedules.js +115 -6
  123. package/dist/edge/http/settings.js +84 -6
  124. package/dist/edge/http/storage-files.js +50 -0
  125. package/dist/edge/http/storage.js +43 -2
  126. package/dist/edge/http/studio.js +287 -0
  127. package/dist/edge/http/style-preview.js +61 -0
  128. package/dist/edge/http/subtitles.js +1 -1
  129. package/dist/edge/http/trash.js +69 -0
  130. package/dist/edge/http/tutorials.js +59 -0
  131. package/dist/edge/http/update.js +24 -3
  132. package/dist/edge/http/whats-new.js +15 -0
  133. package/dist/edge/http/youtube-edits.js +92 -0
  134. package/dist/edge/native-update.js +62 -0
  135. package/dist/extension/slopify-studio-chrome.zip +0 -0
  136. package/dist/extension/slopify-studio-firefox.zip +0 -0
  137. package/dist/host-cli/gemini-login.js +83 -0
  138. package/dist/host-cli/open-folder.js +10 -10
  139. package/dist/host-cli/runtime.js +9 -1
  140. package/dist/host-cli/status.js +4 -2
  141. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  142. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  143. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  144. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  145. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  146. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  147. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  148. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  149. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  150. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  151. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  152. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  153. package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
  154. package/dist/kernel/paths.js +27 -6
  155. package/dist/kernel/pipeline.js +4 -1
  156. package/dist/kernel/ports/host-cli.js +122 -14
  157. package/dist/kernel/ports/key-probe.js +1 -0
  158. package/dist/kernel/ports/languages.js +157 -0
  159. package/dist/kernel/ports/model.js +10 -5
  160. package/dist/kernel/ports/narration-aliases.js +60 -0
  161. package/dist/kernel/ports/plan-limits.js +36 -0
  162. package/dist/kernel/ports/system-speech.js +203 -0
  163. package/dist/kernel/runner/attempt.js +17 -8
  164. package/dist/kernel/runner/audition.js +36 -0
  165. package/dist/kernel/runner/graph.js +40 -5
  166. package/dist/kernel/runner/index.js +39 -9
  167. package/dist/kernel/runner/meter.js +1 -0
  168. package/dist/kernel/runner/providers.js +142 -7
  169. package/dist/kernel/runner/retry-policy.js +43 -0
  170. package/dist/kernel/runner/standalone.js +134 -0
  171. package/dist/kernel/runner/work-authority.js +28 -2
  172. package/dist/main.js +277 -16
  173. package/dist/model-catalog.js +2 -0
  174. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  175. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  176. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  177. package/dist/patch-notes/3.0.0.md +70 -0
  178. package/dist/patch-notes/3.0.1.md +105 -0
  179. package/dist/patch-notes/index.json +32 -0
  180. package/dist/slices/admission/model.js +27 -0
  181. package/dist/slices/admission/repo.js +24 -4
  182. package/dist/slices/admission/rules.js +80 -2
  183. package/dist/slices/admission/schema.js +58 -1
  184. package/dist/slices/admission/short-mode.js +47 -0
  185. package/dist/slices/admission/start.js +42 -6
  186. package/dist/slices/article/continuation.js +2 -2
  187. package/dist/slices/article/provided-entries.js +2 -2
  188. package/dist/slices/article/run.js +6 -2
  189. package/dist/slices/article/segments.js +3 -3
  190. package/dist/slices/backups/folder.js +14 -6
  191. package/dist/slices/batch/index.js +7 -2
  192. package/dist/slices/cancel/index.js +1 -1
  193. package/dist/slices/channels/cast-images.js +110 -0
  194. package/dist/slices/channels/cast-match.js +32 -0
  195. package/dist/slices/channels/images.js +19 -0
  196. package/dist/slices/channels/model.js +5 -0
  197. package/dist/slices/channels/rebrand.js +100 -0
  198. package/dist/slices/channels/repo.js +221 -0
  199. package/dist/slices/channels/runs.js +129 -0
  200. package/dist/slices/channels/schema.js +152 -0
  201. package/dist/slices/channels/service.js +180 -0
  202. package/dist/slices/channels/studio-csv.js +110 -0
  203. package/dist/slices/channels/videos.js +120 -0
  204. package/dist/slices/checkpoints/recovery.js +10 -0
  205. package/dist/slices/control/index.js +5 -2
  206. package/dist/slices/control/providers.js +4 -1
  207. package/dist/slices/document/fonts.js +1 -1
  208. package/dist/slices/document/render.js +1 -1
  209. package/dist/slices/episodes/related.js +90 -0
  210. package/dist/slices/episodes/repo.js +60 -0
  211. package/dist/slices/episodes/service.js +58 -0
  212. package/dist/slices/episodes/summarize.js +170 -0
  213. package/dist/slices/estimate/index.js +109 -28
  214. package/dist/slices/estimate/requests.js +68 -2
  215. package/dist/slices/eta/history.js +56 -0
  216. package/dist/slices/eta/model.js +63 -0
  217. package/dist/slices/eta/view.js +25 -0
  218. package/dist/slices/fixes/rules.js +73 -0
  219. package/dist/slices/fonts/coverage.js +82 -0
  220. package/dist/slices/fonts/sfnt.js +74 -0
  221. package/dist/slices/images/run.js +8 -5
  222. package/dist/slices/images/scale.js +84 -0
  223. package/dist/slices/library/diff.js +122 -0
  224. package/dist/slices/library/history.js +100 -0
  225. package/dist/slices/library/model.js +3 -0
  226. package/dist/slices/library/photorealistic.js +49 -0
  227. package/dist/slices/library/repo.js +19 -14
  228. package/dist/slices/library/save.js +62 -12
  229. package/dist/slices/library/slots.js +7 -1
  230. package/dist/slices/library/used-by.js +153 -0
  231. package/dist/slices/loudness/level-pieces.js +41 -0
  232. package/dist/slices/loudness/loudnorm.js +164 -0
  233. package/dist/slices/loudness/model.js +153 -0
  234. package/dist/slices/model-upkeep/model.js +10 -0
  235. package/dist/slices/model-upkeep/switch.js +217 -0
  236. package/dist/slices/model-upkeep/usage.js +185 -0
  237. package/dist/slices/narration/aliases-library.js +31 -0
  238. package/dist/slices/narration/aliases-schema.js +53 -0
  239. package/dist/slices/narration/aliases.js +49 -0
  240. package/dist/slices/narration/blocks.js +352 -0
  241. package/dist/slices/narration/chunk.js +19 -7
  242. package/dist/slices/narration/concat.js +2 -2
  243. package/dist/slices/narration/describe.js +45 -0
  244. package/dist/slices/narration/pauses-model.js +63 -0
  245. package/dist/slices/narration/pauses.js +269 -0
  246. package/dist/slices/narration/peaks-model.js +1 -0
  247. package/dist/slices/narration/peaks.js +94 -0
  248. package/dist/slices/narration/preparation.js +21 -3
  249. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  250. package/dist/slices/narration/pronunciation.js +72 -25
  251. package/dist/slices/narration/run.js +6 -6
  252. package/dist/slices/narration/shared-glossary.js +1 -1
  253. package/dist/slices/narration/steering.js +15 -4
  254. package/dist/slices/notifications/notifier.js +47 -3
  255. package/dist/slices/notifications/rules.js +48 -3
  256. package/dist/slices/onboarding/copy.js +143 -0
  257. package/dist/slices/onboarding/first-run.js +72 -0
  258. package/dist/slices/onboarding/full-video.js +81 -0
  259. package/dist/slices/onboarding/install.js +119 -0
  260. package/dist/slices/onboarding/model.js +85 -0
  261. package/dist/slices/onboarding/packs.js +491 -0
  262. package/dist/slices/onboarding/quick-short.js +144 -0
  263. package/dist/slices/onboarding/sample.js +132 -0
  264. package/dist/slices/onboarding/state.js +68 -0
  265. package/dist/slices/onboarding/template.js +73 -0
  266. package/dist/slices/patch-notes/library.js +73 -0
  267. package/dist/slices/patch-notes/seen.js +77 -0
  268. package/dist/slices/play-drafts/convert.js +127 -8
  269. package/dist/slices/play-drafts/readiness.js +4 -1
  270. package/dist/slices/play-drafts/repo.js +2 -0
  271. package/dist/slices/play-drafts/review-inputs.js +20 -3
  272. package/dist/slices/play-drafts/schema.js +89 -0
  273. package/dist/slices/play-drafts/service.js +4 -1
  274. package/dist/slices/play-drafts/start.js +43 -18
  275. package/dist/slices/project-templates/from-project.js +59 -6
  276. package/dist/slices/project-templates/next-chapter.js +94 -0
  277. package/dist/slices/project-templates/one-off.js +26 -0
  278. package/dist/slices/project-templates/repo.js +2 -1
  279. package/dist/slices/project-templates/schema.js +7 -1
  280. package/dist/slices/project-templates/service.js +15 -4
  281. package/dist/slices/project-templates/setup.js +18 -2
  282. package/dist/slices/rebuild/admission-repo.js +1 -1
  283. package/dist/slices/rebuild/model.js +6 -0
  284. package/dist/slices/rebuild/preview-details.js +46 -3
  285. package/dist/slices/rebuild/preview-plan.js +13 -6
  286. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  287. package/dist/slices/rebuild/recipe-audio.js +66 -12
  288. package/dist/slices/rebuild/recipe-build.js +8 -4
  289. package/dist/slices/rebuild/recipe-cast.js +18 -0
  290. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  291. package/dist/slices/rebuild/recipe-describe.js +93 -0
  292. package/dist/slices/rebuild/recipe-edit.js +13 -1
  293. package/dist/slices/rebuild/recipe-exports.js +67 -8
  294. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  295. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  296. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  297. package/dist/slices/rebuild/recipe-model.js +25 -6
  298. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  299. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  300. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  301. package/dist/slices/rebuild/recipe-reference.js +15 -1
  302. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  303. package/dist/slices/rebuild/recipe-save.js +4 -1
  304. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  305. package/dist/slices/rebuild/recipe-text.js +114 -19
  306. package/dist/slices/rebuild/recipe-validation.js +14 -2
  307. package/dist/slices/rebuild/recipe-visual.js +96 -33
  308. package/dist/slices/rebuild/recipe-voices.js +286 -0
  309. package/dist/slices/rebuild/recipe-work.js +77 -6
  310. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  311. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  312. package/dist/slices/rebuild/recovery-model.js +3 -0
  313. package/dist/slices/rebuild/recovery-selection.js +28 -2
  314. package/dist/slices/rebuild/recovery.js +35 -17
  315. package/dist/slices/rebuild/repo.js +3 -3
  316. package/dist/slices/rebuild/retry.js +18 -0
  317. package/dist/slices/rebuild/review-redo.js +125 -0
  318. package/dist/slices/rebuild/runtime-actions.js +5 -0
  319. package/dist/slices/rebuild/runtime-admission.js +4 -1
  320. package/dist/slices/rebuild/runtime-animate.js +4 -4
  321. package/dist/slices/rebuild/runtime-article.js +5 -5
  322. package/dist/slices/rebuild/runtime-document.js +4 -4
  323. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  324. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  325. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  326. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  327. package/dist/slices/rebuild/runtime-export.js +84 -19
  328. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  329. package/dist/slices/rebuild/runtime-image.js +21 -2
  330. package/dist/slices/rebuild/runtime-local.js +148 -24
  331. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  332. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  333. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  334. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  335. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  336. package/dist/slices/rebuild/runtime-provider.js +126 -18
  337. package/dist/slices/rebuild/runtime-publication.js +21 -5
  338. package/dist/slices/rebuild/runtime-review.js +267 -0
  339. package/dist/slices/rebuild/runtime-run.js +17 -11
  340. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  341. package/dist/slices/rebuild/runtime-store.js +62 -11
  342. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  343. package/dist/slices/rebuild/runtime-voices.js +103 -0
  344. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  345. package/dist/slices/rebuild/service-readiness.js +3 -3
  346. package/dist/slices/rebuild/soften.js +49 -0
  347. package/dist/slices/rebuild/transition-repo.js +9 -2
  348. package/dist/slices/research/run.js +4 -4
  349. package/dist/slices/research/synthesis.js +2 -2
  350. package/dist/slices/reviews/model.js +105 -0
  351. package/dist/slices/reviews/outcome.js +22 -0
  352. package/dist/slices/reviews/repo.js +118 -0
  353. package/dist/slices/reviews/rules.js +63 -0
  354. package/dist/slices/reviews/verdict.js +71 -0
  355. package/dist/slices/reviews/view.js +30 -0
  356. package/dist/slices/revisions/adopt-content.js +5 -2
  357. package/dist/slices/revisions/adopt.js +3 -1
  358. package/dist/slices/revisions/downloads.js +7 -1
  359. package/dist/slices/revisions/image-plan.js +183 -0
  360. package/dist/slices/revisions/mutation-assets.js +10 -0
  361. package/dist/slices/revisions/mutation-cues.js +71 -0
  362. package/dist/slices/revisions/mutation-request.js +2 -1
  363. package/dist/slices/revisions/mutations.js +97 -3
  364. package/dist/slices/revisions/schema.js +11 -1
  365. package/dist/slices/run-cost/limits.js +128 -0
  366. package/dist/slices/run-cost/meter.js +47 -0
  367. package/dist/slices/run-cost/panel.js +256 -0
  368. package/dist/slices/run-cost/pricing.js +95 -0
  369. package/dist/slices/run-cost/week.js +108 -0
  370. package/dist/slices/schedules/agenda.js +222 -0
  371. package/dist/slices/schedules/repo.js +58 -18
  372. package/dist/slices/schedules/scheduler.js +68 -17
  373. package/dist/slices/schedules/schema.js +179 -1
  374. package/dist/slices/schedules/service.js +43 -4
  375. package/dist/slices/schedules/similar.js +128 -0
  376. package/dist/slices/schedules/topic-list.js +163 -0
  377. package/dist/slices/schedules/topics.js +523 -0
  378. package/dist/slices/settings/first-run.js +106 -0
  379. package/dist/slices/settings/health.js +247 -0
  380. package/dist/slices/settings/key-guides.js +138 -0
  381. package/dist/slices/settings/key-test.js +93 -0
  382. package/dist/slices/settings/keys.js +10 -5
  383. package/dist/slices/settings/model-reach.js +52 -0
  384. package/dist/slices/settings/model.js +55 -1
  385. package/dist/slices/settings/models.js +5 -1
  386. package/dist/slices/settings/playback.js +15 -0
  387. package/dist/slices/settings/readiness.js +14 -2
  388. package/dist/slices/settings/repo.js +17 -3
  389. package/dist/slices/settings/tutorial-schema.js +5 -0
  390. package/dist/slices/settings/voices.js +64 -4
  391. package/dist/slices/settings/whats-new.js +64 -0
  392. package/dist/slices/shorts/captions.js +9 -0
  393. package/dist/slices/shorts/pick.js +3 -3
  394. package/dist/slices/shorts/prompts.js +1 -1
  395. package/dist/slices/shorts/render.js +63 -13
  396. package/dist/slices/storage/asset-name.js +3 -0
  397. package/dist/slices/storage/backup-export.js +48 -9
  398. package/dist/slices/storage/backup-format.js +38 -1
  399. package/dist/slices/storage/backup-import.js +191 -19
  400. package/dist/slices/storage/delete-project.js +7 -4
  401. package/dist/slices/storage/documents.js +87 -0
  402. package/dist/slices/storage/downloads.js +33 -1
  403. package/dist/slices/storage/files-location.js +531 -0
  404. package/dist/slices/storage/layout.js +14 -4
  405. package/dist/slices/storage/model.js +11 -0
  406. package/dist/slices/storage/portable.js +94 -12
  407. package/dist/slices/storage/reconcile.js +14 -1
  408. package/dist/slices/storage/repo.js +4 -1
  409. package/dist/slices/storage/schema.js +11 -0
  410. package/dist/slices/storage/trim.js +146 -0
  411. package/dist/slices/studio/disclosure.js +74 -0
  412. package/dist/slices/studio/model.js +30 -0
  413. package/dist/slices/studio/pack.js +211 -0
  414. package/dist/slices/studio/queue.js +69 -0
  415. package/dist/slices/studio/settings.js +170 -0
  416. package/dist/slices/style-preview/images.js +61 -0
  417. package/dist/slices/style-preview/narration.js +33 -0
  418. package/dist/slices/style-preview/render.js +148 -0
  419. package/dist/slices/style-preview/schema.js +59 -0
  420. package/dist/slices/style-preview/service.js +92 -0
  421. package/dist/slices/style-preview/settings.js +139 -0
  422. package/dist/slices/subtitles/captions.js +42 -9
  423. package/dist/slices/subtitles/model.js +22 -0
  424. package/dist/slices/subtitles/prepare.js +27 -16
  425. package/dist/slices/subtitles/transcript.js +1 -1
  426. package/dist/slices/thumbnail/run.js +7 -7
  427. package/dist/slices/trash/model.js +25 -0
  428. package/dist/slices/trash/service.js +273 -0
  429. package/dist/slices/tutorials/anchors.js +57 -0
  430. package/dist/slices/tutorials/library.js +185 -0
  431. package/dist/slices/uploads/repo.js +20 -0
  432. package/dist/slices/video/ambient-bed-schema.js +15 -0
  433. package/dist/slices/video/ambient-bed.js +95 -0
  434. package/dist/slices/video/ambient-mix.js +72 -0
  435. package/dist/slices/video/audio-inputs.js +2 -2
  436. package/dist/slices/video/cards.js +5 -2
  437. package/dist/slices/video/edit-list.js +23 -2
  438. package/dist/slices/video/edit-settings.js +13 -3
  439. package/dist/slices/video/ffmpeg.js +63 -9
  440. package/dist/slices/video/figure-card.js +492 -0
  441. package/dist/slices/video/figure-spans.js +51 -0
  442. package/dist/slices/video/plan.js +125 -13
  443. package/dist/slices/video/run.js +14 -12
  444. package/dist/slices/video/slideshow.js +39 -4
  445. package/dist/slices/voices/attribution.js +69 -0
  446. package/dist/slices/voices/audio-files.js +87 -0
  447. package/dist/slices/voices/audition.js +25 -0
  448. package/dist/slices/voices/cast.js +74 -0
  449. package/dist/slices/voices/grouping.js +59 -0
  450. package/dist/slices/voices/join.js +35 -0
  451. package/dist/slices/voices/languages.js +25 -0
  452. package/dist/slices/voices/model.js +218 -0
  453. package/dist/slices/voices/palette.js +26 -0
  454. package/dist/slices/voices/panel.js +91 -0
  455. package/dist/slices/voices/portraits.js +55 -0
  456. package/dist/slices/voices/script.js +136 -0
  457. package/dist/slices/voices/timing.js +58 -0
  458. package/dist/slices/youtube/answer.js +1 -1
  459. package/dist/slices/youtube/chapters.js +126 -0
  460. package/dist/slices/youtube/edits-repo.js +84 -0
  461. package/dist/slices/youtube/edits.js +89 -0
  462. package/dist/slices/youtube/placeholders.js +98 -0
  463. package/dist/slices/youtube/transcript.js +11 -4
  464. package/dist/tutorials/AI-CLIs.md +146 -0
  465. package/dist/tutorials/Backups.md +121 -0
  466. package/dist/tutorials/Calendar.md +103 -0
  467. package/dist/tutorials/Cast-Library.md +112 -0
  468. package/dist/tutorials/Channels.md +148 -0
  469. package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
  470. package/dist/tutorials/Docker.md +171 -0
  471. package/dist/tutorials/Document-Themes.md +207 -0
  472. package/dist/tutorials/Editing-a-Project.md +204 -0
  473. package/dist/tutorials/FAQ.md +134 -0
  474. package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
  475. package/dist/tutorials/Home-and-Projects.md +174 -0
  476. package/dist/tutorials/Home.md +119 -0
  477. package/dist/tutorials/Install.md +154 -0
  478. package/dist/tutorials/Intros-and-Outros.md +86 -0
  479. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
  480. package/dist/tutorials/Library-Overview.md +75 -0
  481. package/dist/tutorials/Models.md +133 -0
  482. package/dist/tutorials/Multiple-Voices.md +161 -0
  483. package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
  484. package/dist/tutorials/Notifications.md +86 -0
  485. package/dist/tutorials/Other-Languages.md +110 -0
  486. package/dist/tutorials/PDF-Documents.md +100 -0
  487. package/dist/tutorials/Play-Images.md +135 -0
  488. package/dist/tutorials/Play-Narration.md +167 -0
  489. package/dist/tutorials/Play-Outputs.md +109 -0
  490. package/dist/tutorials/Play-Overview.md +216 -0
  491. package/dist/tutorials/Play-Title-and-Article.md +135 -0
  492. package/dist/tutorials/Play-Video-and-Style.md +143 -0
  493. package/dist/tutorials/Project-Page.md +228 -0
  494. package/dist/tutorials/Prompts.md +136 -0
  495. package/dist/tutorials/Providers-and-Keys.md +290 -0
  496. package/dist/tutorials/Publishing-to-YouTube.md +135 -0
  497. package/dist/tutorials/Recovery-and-Retries.md +141 -0
  498. package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
  499. package/dist/tutorials/Schedules.md +154 -0
  500. package/dist/tutorials/Settings-Reference.md +258 -0
  501. package/dist/tutorials/Shorts.md +116 -0
  502. package/dist/tutorials/Start-at-Login.md +126 -0
  503. package/dist/tutorials/Studio-Extension.md +146 -0
  504. package/dist/tutorials/Templates.md +146 -0
  505. package/dist/tutorials/Trash.md +87 -0
  506. package/dist/tutorials/Troubleshooting.md +149 -0
  507. package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
  508. package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
  509. package/dist/tutorials/Video-Editing.md +135 -0
  510. package/dist/tutorials/Where-Your-Files-Live.md +149 -0
  511. package/dist/tutorials/YouTube-Description.md +132 -0
  512. package/dist/tutorials/Your-First-Short.md +125 -0
  513. package/dist/tutorials/_Footer.md +1 -0
  514. package/dist/tutorials/_Sidebar.md +69 -0
  515. package/dist/updater/service.js +66 -21
  516. package/dist/web/assets/index-DBLSfqA-.css +1 -0
  517. package/dist/web/assets/index-DFdIhmm0.js +307 -0
  518. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  519. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  520. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  521. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  522. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  523. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  524. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  525. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  526. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  527. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
  528. package/dist/web/index.html +2 -2
  529. package/package.json +5 -5
  530. package/dist/edge/docker-launch.js +0 -81
  531. package/dist/edge/docker-projects/activation.js +0 -35
  532. package/dist/edge/docker-projects/claims.js +0 -85
  533. package/dist/edge/docker-projects/committed.js +0 -40
  534. package/dist/edge/docker-projects/engine.js +0 -461
  535. package/dist/edge/docker-projects/install.js +0 -254
  536. package/dist/edge/docker-projects/prune.js +0 -66
  537. package/dist/edge/docker-projects/recover.js +0 -100
  538. package/dist/edge/docker-projects/state.js +0 -225
  539. package/dist/web/assets/index-CktpxlJd.js +0 -171
  540. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  541. package/scripts/docker-run.sh +0 -3
  542. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -14,7 +14,8 @@ export function ensureBaseline(deps, projectId) {
14
14
  return withProjectControl(deps.db, projectId, () => adoptBaseline(deps, projectId));
15
15
  }
16
16
  export function adoptBaseline(deps, projectId, templates,
17
- // The Shorts step's background music a new run was started with, already a project asset.
17
+ // The Shorts step's background music and the ambient bed's own file a new run was started
18
+ // with, already project assets.
18
19
  supplied = {}) {
19
20
  return transact(deps.db, () => {
20
21
  const project = projectById(deps.db, projectId);
@@ -34,6 +35,7 @@ supplied = {}) {
34
35
  const content = {
35
36
  ...baselineContent(deps, project, outputs, pieces, assets, templates),
36
37
  ...(supplied.shortsMusic === undefined ? {} : { shortsMusic: supplied.shortsMusic }),
38
+ ...(supplied.ambientBed === undefined ? {} : { ambientBed: supplied.ambientBed }),
37
39
  };
38
40
  const fingerprints = baselineFingerprints(project, outputs, pieces, content, stages);
39
41
  const revision = {
@@ -3,6 +3,7 @@ import { extname } from "node:path";
3
3
  import { zipSync } from "fflate";
4
4
  import { z } from "zod";
5
5
  import { stageKinds } from "../../kernel/pipeline.js";
6
+ import { thumbnailCountOf } from "../admission/model.js";
6
7
  import { contentTypeOf, downloadName, slugOf } from "../storage/downloads.js";
7
8
  import { outputPath } from "../storage/layout.js";
8
9
  import { projectTitle } from "../storage/repo.js";
@@ -47,7 +48,12 @@ export function revisionImagesZip(deps, projectId, revisionId) {
47
48
  const slug = slugOf(revision.config.title);
48
49
  const entries = {};
49
50
  const images = outputsForRevision(deps.db, projectId, revisionId)
50
- .filter((row) => row.selected && (row.output.role === "image" || row.output.role === "thumbnail"))
51
+ .filter((row) => row.selected &&
52
+ (row.output.role === "image" ||
53
+ // Every thumbnail the project makes; a second or third left from when it made three
54
+ // stays out once it makes one.
55
+ (row.output.role === "thumbnail" &&
56
+ (row.output.meta.index ?? 1) <= thumbnailCountOf(revision.config))))
51
57
  .map((row) => positionedOutput(revision, row))
52
58
  .sort((a, b) => imageOrder(a.output.role, a.output.meta.index) -
53
59
  imageOrder(b.output.role, b.output.meta.index));
@@ -0,0 +1,183 @@
1
+ // Image prompts and their Numbers changed in Edit project. A project's images were planned per
2
+ // prompt when its run was adopted (`adopt-content.ts`): each image is a definition under
3
+ // `imagePrompts.N`, Number times per prompt. Changing which prompts are used, or a Number,
4
+ // re-derives those definitions here, the way Play's plan lays them out (prompts in selection
5
+ // order, Number images each), while keeping every definition a prompt already has: an image
6
+ // keeps its key and its wording, so its fingerprint - and the image made for it - stays.
7
+ //
8
+ // The edit's content is read in the numbering of `from` (the prompts the revision was saved
9
+ // with); the result is in the numbering of `to`. The browser calls this too, with stand-in
10
+ // keys, to say what saving will add and remove.
11
+ import { imagesPerRunMax, numberPerPromptMax } from "../admission/rules.js";
12
+ import { render } from "../admission/substitute.js";
13
+ export function imagePromptKey(index) {
14
+ return `imagePrompts.${String(index)}`;
15
+ }
16
+ function promptIndex(key) {
17
+ const match = /^imagePrompts\.(\d+)$/.exec(key ?? "");
18
+ return match?.[1] === undefined ? undefined : Number(match[1]);
19
+ }
20
+ export function sameImagePrompts(from, to) {
21
+ return (from.length === to.length &&
22
+ from.every((one, index) => one.name === to[index]?.name && one.number === to[index]?.number));
23
+ }
24
+ // The same bounds Play's admission holds a run to.
25
+ export function imagePromptFields(to) {
26
+ const fields = [];
27
+ if (to.length === 0)
28
+ fields.push({
29
+ field: "imagePrompts",
30
+ message: "Tick at least one image prompt under Images, or set Images to Off or Provide on Inputs.",
31
+ });
32
+ const names = new Set();
33
+ for (const [index, prompt] of to.entries()) {
34
+ if (prompt.name.trim() === "")
35
+ fields.push({
36
+ field: `imagePrompts.${String(index)}.name`,
37
+ message: "Pick an image prompt.",
38
+ });
39
+ else if (names.has(prompt.name))
40
+ fields.push({
41
+ field: `imagePrompts.${String(index)}.name`,
42
+ message: `The image prompt "${prompt.name}" is ticked twice. Untick one under Images.`,
43
+ });
44
+ names.add(prompt.name);
45
+ if (!Number.isInteger(prompt.number) || prompt.number < 1 || prompt.number > numberPerPromptMax)
46
+ fields.push({
47
+ field: `imagePrompts.${String(index)}.number`,
48
+ message: `Enter a whole number between 1 and ${String(numberPerPromptMax)} for "${prompt.name}" under Images.`,
49
+ });
50
+ }
51
+ const total = to.reduce((sum, prompt) => sum + prompt.number, 0);
52
+ if (total > imagesPerRunMax)
53
+ fields.push({
54
+ field: "imagePrompts",
55
+ message: `A video can have at most ${String(imagesPerRunMax)} images; these prompts ask for ${String(total)}. Lower the Numbers under Images.`,
56
+ });
57
+ return fields;
58
+ }
59
+ export function replanImagePrompts(input) {
60
+ const { from, to, content } = input;
61
+ // Where each prompt of `to` was in `from`, by name: a prompt is the same prompt while its
62
+ // name is, wherever it now sits.
63
+ const was = to.map((prompt) => from.findIndex((one) => one.name === prompt.name));
64
+ const fields = [];
65
+ const templates = {};
66
+ const rendered = {};
67
+ for (const [key, value] of Object.entries(content.promptTemplates))
68
+ if (promptIndex(key) === undefined)
69
+ templates[key] = value;
70
+ for (const [key, value] of Object.entries(input.rendered))
71
+ if (promptIndex(key) === undefined)
72
+ rendered[key] = value;
73
+ for (const [index, prompt] of to.entries()) {
74
+ const key = imagePromptKey(index);
75
+ const old = was[index] ?? -1;
76
+ if (old >= 0) {
77
+ const oldKey = imagePromptKey(old);
78
+ if (Object.hasOwn(content.promptTemplates, oldKey))
79
+ templates[key] = content.promptTemplates[oldKey] ?? null;
80
+ const text = input.rendered[oldKey];
81
+ if (text !== undefined)
82
+ rendered[key] = text;
83
+ continue;
84
+ }
85
+ const body = input.body(prompt.name);
86
+ if (body === undefined) {
87
+ fields.push({
88
+ field: `imagePrompts.${String(index)}.name`,
89
+ message: `The image prompt "${prompt.name}" is not in the Library any more. Untick it under Images, or add it back on Library → Prompts.`,
90
+ });
91
+ continue;
92
+ }
93
+ templates[key] = body;
94
+ rendered[key] = render(body, input.values);
95
+ }
96
+ if (fields.length > 0)
97
+ return { ok: false, fields };
98
+ // The definitions each prompt of `from` has now, in slideshow order.
99
+ const groups = new Map();
100
+ for (const key of content.imageOrder) {
101
+ const index = promptIndex(content.imageDefinitions[key]?.templateKey);
102
+ if (index === undefined || index >= from.length)
103
+ continue;
104
+ groups.set(index, [...(groups.get(index) ?? []), key]);
105
+ }
106
+ const definitions = {};
107
+ const added = [];
108
+ const removed = [];
109
+ // New images for a prompt: its first image's wording when it has one, else the prompt's own.
110
+ const fresh = (index, like) => {
111
+ const key = imagePromptKey(index);
112
+ const template = templates[key];
113
+ return like === undefined
114
+ ? {
115
+ source: "generate",
116
+ assetId: null,
117
+ prompt: rendered[key] ?? null,
118
+ templateKey: template === undefined || template === null ? null : key,
119
+ }
120
+ : { ...like, source: "generate", assetId: null, templateKey: like.templateKey ?? null };
121
+ };
122
+ const extra = (index, count, like) => Array.from({ length: Math.max(0, count) }, () => {
123
+ const id = input.key();
124
+ definitions[id] = fresh(index, like);
125
+ added.push(id);
126
+ return id;
127
+ });
128
+ const order = [];
129
+ for (const key of content.imageOrder) {
130
+ const definition = content.imageDefinitions[key];
131
+ if (definition === undefined)
132
+ continue;
133
+ const old = promptIndex(definition.templateKey);
134
+ if (old === undefined || old >= from.length) {
135
+ // An image added on its own, or uploaded: not any prompt's, so it stays where it is.
136
+ definitions[key] = definition;
137
+ order.push(key);
138
+ continue;
139
+ }
140
+ const index = was.indexOf(old);
141
+ if (index === -1) {
142
+ removed.push(key);
143
+ continue;
144
+ }
145
+ const group = groups.get(old) ?? [];
146
+ const changed = from[old]?.number !== to[index]?.number;
147
+ const keep = changed ? (to[index]?.number ?? 0) : group.length;
148
+ const at = group.indexOf(key);
149
+ if (at >= keep) {
150
+ removed.push(key);
151
+ continue;
152
+ }
153
+ const moved = definition.templateKey === imagePromptKey(index)
154
+ ? definition
155
+ : { ...definition, templateKey: imagePromptKey(index) };
156
+ definitions[key] = moved;
157
+ order.push(key);
158
+ // A raised Number adds its images right after the prompt's last one.
159
+ if (changed && at === Math.min(group.length, keep) - 1)
160
+ order.push(...extra(index, keep - group.length, moved));
161
+ }
162
+ for (const [index, prompt] of to.entries()) {
163
+ const old = was[index] ?? -1;
164
+ const group = old >= 0 ? (groups.get(old) ?? []) : [];
165
+ // A prompt with no image left (every one removed by hand) makes its Number again only when
166
+ // the Number changed; a prompt new to the project makes all of them, at the end.
167
+ if (old >= 0 && (group.length > 0 || from[old]?.number === prompt.number))
168
+ continue;
169
+ order.push(...extra(index, prompt.number, undefined));
170
+ }
171
+ return {
172
+ ok: true,
173
+ content: {
174
+ ...content,
175
+ imageOrder: order,
176
+ imageDefinitions: definitions,
177
+ promptTemplates: templates,
178
+ },
179
+ rendered,
180
+ added,
181
+ removed,
182
+ };
183
+ }
@@ -144,6 +144,16 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
144
144
  field: "content.shortsMusic",
145
145
  message: "This music file does not belong to this project. Reload the page and choose the music again.",
146
146
  });
147
+ // The ambient bed's own file is only ever the one the project was started with.
148
+ const bed = content.ambientBed;
149
+ if (bed !== undefined &&
150
+ deps.db
151
+ .prepare("SELECT 1 FROM project_revisions WHERE project_id=? AND json_extract(content,'$.ambientBed')=? LIMIT 1")
152
+ .get(projectId, bed) === undefined)
153
+ fields.push({
154
+ field: "content.ambientBed",
155
+ message: "This ambient sound file does not belong to this project. Reload the page and save again.",
156
+ });
147
157
  return fields;
148
158
  }
149
159
  export async function measureAudio(deps, projectId, path) {
@@ -1,6 +1,59 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { z } from "zod";
4
+ import { outputPath } from "../storage/layout.js";
5
+ import { usesVoices } from "../voices/model.js";
6
+ import { cueSpeaker } from "../voices/timing.js";
2
7
  import { assetPath, measureAudio } from "./mutation-assets.js";
3
8
  import { validateCues } from "./rules.js";
9
+ // Edited captions of a multi-voice run keep their speakers. A cue that comes back without one
10
+ // (the caption editor edits text and times only) takes the speaker it had before, found by the
11
+ // cue's id, and a new cue the speaker of the narration under it, from the word timing. A cue
12
+ // sent with a speaker keeps it. Runs with one voice are left as they are.
13
+ export function withCueSpeakers(deps, base, edit) {
14
+ const edited = edit.content.subtitleCues;
15
+ if (edited === undefined ||
16
+ !usesVoices(edit.config) ||
17
+ !cuesChanged(base, edit) ||
18
+ edited.cues.every((cue) => cue.speaker !== undefined))
19
+ return edit;
20
+ const before = new Map((base.revision.content.subtitleCues?.cues ?? []).flatMap((cue) => cue.speaker === undefined ? [] : [[cue.id, cue.speaker]]));
21
+ let words;
22
+ const timing = () => {
23
+ if (words !== undefined)
24
+ return words;
25
+ const row = base.outputs.find((one) => one.selected &&
26
+ one.available &&
27
+ one.state === "ready" &&
28
+ one.workKey === "subtitles:timing" &&
29
+ one.output.role === "subtitle_words");
30
+ try {
31
+ words =
32
+ row === undefined
33
+ ? []
34
+ : timedWords.parse(JSON.parse(readFileSync(outputPath(deps.paths, base.revision.projectId, row.output.path), "utf8"))).words;
35
+ }
36
+ catch {
37
+ // Unreadable timing leaves the new cues without a speaker; they still save.
38
+ words = [];
39
+ }
40
+ return words;
41
+ };
42
+ const cues = edited.cues.map((cue) => {
43
+ if (cue.speaker !== undefined)
44
+ return cue;
45
+ const speaker = before.get(cue.id) ?? cueSpeaker(cue, timing() ?? []);
46
+ return speaker === undefined ? cue : { ...cue, speaker };
47
+ });
48
+ return { ...edit, content: { ...edit.content, subtitleCues: { ...edited, cues } } };
49
+ }
50
+ const timedWords = z.object({
51
+ words: z.array(z.object({
52
+ start: z.number(),
53
+ end: z.number(),
54
+ speaker: z.string().optional(),
55
+ })),
56
+ });
4
57
  export function cuesChanged(base, edit) {
5
58
  return (edit.content.subtitleCues !== undefined &&
6
59
  !isDeepStrictEqual(base.revision.content.subtitleCues, edit.content.subtitleCues));
@@ -54,12 +107,30 @@ export function validateProposedCues(base, edit, plan, prepared, durations) {
54
107
  return undefined;
55
108
  if (recipe.input.kind === "provided" && recipe.input.assetId !== null)
56
109
  return durations.get(recipe.input.assetId);
110
+ // Pauses between sentences lengthen the join by what only the join measures.
111
+ if (recipe.input.kind === "local" &&
112
+ Array.isArray(recipe.input.values) &&
113
+ recipe.input.values[2] !== undefined)
114
+ return undefined;
57
115
  if (recipe.input.kind === "local" && recipe.input.operation === "concat-narration") {
58
116
  const parts = recipe.dependsOn.map(durationFor);
59
117
  return parts.some((one) => one === undefined)
60
118
  ? undefined
61
119
  : parts.reduce((sum, one) => sum + (one ?? 0), 0);
62
120
  }
121
+ // Speaker turns: each at its pace, with the gap after it (`recipe-voices.ts`).
122
+ if (recipe.input.kind === "local" && recipe.input.operation === "concat-turns-v1") {
123
+ const layout = Array.isArray(recipe.input.values) ? recipe.input.values[1] : undefined;
124
+ const parts = recipe.dependsOn.map(durationFor);
125
+ if (!Array.isArray(layout) || parts.some((one) => one === undefined))
126
+ return undefined;
127
+ return parts.reduce((sum, one, index) => {
128
+ const [pace, gap] = Array.isArray(layout[index]) ? layout[index] : [1, 0];
129
+ return (sum +
130
+ (one ?? 0) / (typeof pace === "number" && pace > 0 ? pace : 1) +
131
+ (typeof gap === "number" ? gap * 1000 : 0));
132
+ }, 0);
133
+ }
63
134
  if (plan.baseFingerprints[key] !== plan.fingerprints[key])
64
135
  return undefined;
65
136
  const prefix = key.replace(/:1$/, ":");
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { z } from "zod";
3
+ import { projectExists } from "../admission/repo.js";
3
4
  import { currentRevisionId } from "./repo.js";
4
5
  import { getRevisionView } from "./view.js";
5
6
  export function requestHash(operation, baseRevisionId, input) {
@@ -43,7 +44,7 @@ export function checkMutation(deps, input) {
43
44
  .prepare("SELECT 1 FROM project_recovery_requests WHERE project_id=? AND idempotency_key=?")
44
45
  .get(input.projectId, input.idempotencyKey) !== undefined)
45
46
  return refusal(deps, input.projectId, "idempotency-conflict");
46
- if (db.prepare("SELECT 1 FROM projects WHERE id=?").get(input.projectId) === undefined)
47
+ if (!projectExists(db, input.projectId))
47
48
  return refusal(deps, input.projectId, "no-project");
48
49
  if (currentRevisionId(db, input.projectId) !== input.baseRevisionId)
49
50
  return refusal(deps, input.projectId, "conflict");
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { transact } from "../../kernel/db/tx.js";
3
+ import { rebrandedEdit } from "../channels/rebrand.js";
4
+ import { projectChannelId, resolveChannelId, setProjectChannel } from "../channels/repo.js";
3
5
  import { carryCheckpointGates } from "../checkpoints/recovery.js";
6
+ import { imageCountsOf, imageScaleProblem } from "../images/scale.js";
7
+ import { promptByName } from "../library/repo.js";
4
8
  import { narrationRegenerationKey } from "../narration/plan.js";
5
9
  import { recipeInputSchema } from "../rebuild/recipe-input-schema.js";
6
10
  import { planRevision } from "../rebuild/recipe-save.js";
@@ -9,8 +13,10 @@ import { projectStandings } from "../rebuild/runtime-store.js";
9
13
  import { discardPreparedAssets } from "../storage/assets.js";
10
14
  import { deleteStagedFile } from "../storage/repo.js";
11
15
  import { releaseStagedFile } from "../storage/staging-refs.js";
16
+ import { ambientBedProblems } from "../video/ambient-bed.js";
17
+ import { imagePromptFields, replanImagePrompts, sameImagePrompts } from "./image-plan.js";
12
18
  import { validateAssetReferences, validateReplacementAvailability, validateUploads, } from "./mutation-assets.js";
13
- import { cuesChanged, inspectCueAudio, measuredOutputs, validateProposedCues, } from "./mutation-cues.js";
19
+ import { cuesChanged, inspectCueAudio, measuredOutputs, validateProposedCues, withCueSpeakers, } from "./mutation-cues.js";
14
20
  import { prepareEditAssets } from "./mutation-prepare.js";
15
21
  import { checkMutation, insertReceipt, requestHash, requiredView } from "./mutation-request.js";
16
22
  import { preserveDeferredIntent } from "./mutation-work.js";
@@ -43,8 +49,17 @@ export async function saveRevision(deps, input) {
43
49
  if (checked !== undefined)
44
50
  return checked;
45
51
  const base = requiredView(deps, input.projectId, input.baseRevisionId);
46
- const edit = bindNarrationSources(base, submitted);
52
+ const replanned = replannedImages(deps, base, submitted);
53
+ if (!replanned.ok)
54
+ return {
55
+ ok: false,
56
+ reason: "invalid-edit",
57
+ currentRevisionId: base.revision.id,
58
+ fields: replanned.fields,
59
+ };
60
+ const edit = bindNarrationSources(base, rebrandedEdit(deps.db, base.revision.config, replanned.edit));
47
61
  const fields = [
62
+ ...settingProblems(edit),
48
63
  ...validateUploads(deps, edit),
49
64
  ...validateTemplateIntent(base, edit),
50
65
  ...validateNarrationIntent(base, edit),
@@ -60,7 +75,8 @@ export async function saveRevision(deps, input) {
60
75
  currentRevisionId: base.revision.id,
61
76
  fields: unavailable,
62
77
  };
63
- const { edit: supplied, assets: prepared } = await prepareEditAssets(deps, base, edit);
78
+ const { edit: uploaded, assets: prepared } = await prepareEditAssets(deps, base, edit);
79
+ const supplied = withCueSpeakers(deps, base, uploaded);
64
80
  try {
65
81
  const durations = await inspectCueAudio(deps, base, supplied, prepared);
66
82
  const result = transact(deps.db, () => {
@@ -173,12 +189,90 @@ export async function saveRevision(deps, input) {
173
189
  .filter((asset) => deps.db.prepare("SELECT 1 FROM project_assets WHERE id=?").get(asset.id) === undefined));
174
190
  }
175
191
  }
192
+ // Settings Edit project changes that admission checks at Play's Start: the ambient bed's
193
+ // numbers and More images for long videos. Said where Edit project shows them.
194
+ function settingProblems(edit) {
195
+ const bed = edit.config.ambientBed;
196
+ const scale = edit.config.imageScale;
197
+ const scaleProblem = scale === undefined ? undefined : imageScaleProblem(scale);
198
+ return [
199
+ ...(bed === undefined
200
+ ? []
201
+ : ambientBedProblems(bed).map((problem) => ({
202
+ field: `ambientBed.${problem.field}`,
203
+ message: `${problem.message} Change it in Edit project → Inputs → Ambient sound.`,
204
+ }))),
205
+ ...(scaleProblem === undefined
206
+ ? []
207
+ : [
208
+ {
209
+ field: "imageScale",
210
+ message: `${scaleProblem} Change it in Edit project → Images → More images for long videos.`,
211
+ },
212
+ ]),
213
+ ];
214
+ }
215
+ // The image prompts or their Numbers changed: the image definitions are planned again from
216
+ // them (`image-plan.ts`), keeping every image an unchanged prompt already has.
217
+ function replannedImages(deps, base, edit) {
218
+ // Planned in the counts each prompt actually makes: its Number, plus its share of the extra
219
+ // images More images for long videos adds (`images/scale.ts`), as the run was adopted.
220
+ const effective = (config) => {
221
+ const counts = imageCountsOf(config);
222
+ return config.imagePrompts.map((prompt, index) => ({
223
+ ...prompt,
224
+ number: counts[index] ?? prompt.number,
225
+ }));
226
+ };
227
+ if (edit.config.sources.images !== "generate" &&
228
+ sameImagePrompts(base.revision.config.imagePrompts, edit.config.imagePrompts))
229
+ return { ok: true, edit };
230
+ const from = effective(base.revision.config);
231
+ const to = effective(edit.config);
232
+ if (sameImagePrompts(from, to))
233
+ return { ok: true, edit };
234
+ if (edit.config.sources.images !== "generate")
235
+ return {
236
+ ok: false,
237
+ fields: [
238
+ {
239
+ field: "imagePrompts",
240
+ message: "Image prompts are used only while Images is set to Generate. Set Images to Generate on Inputs, or leave the prompts as they were.",
241
+ },
242
+ ],
243
+ };
244
+ const fields = imagePromptFields(edit.config.imagePrompts);
245
+ if (fields.length > 0)
246
+ return { ok: false, fields };
247
+ const plan = replanImagePrompts({
248
+ from,
249
+ to,
250
+ content: edit.content,
251
+ rendered: edit.config.rendered,
252
+ values: edit.config.values,
253
+ body: (name) => promptByName(deps.db, "image", name)?.body,
254
+ key: () => deps.ids.next(),
255
+ });
256
+ if (!plan.ok)
257
+ return plan;
258
+ return {
259
+ ok: true,
260
+ edit: { ...edit, config: { ...edit.config, rendered: plan.rendered }, content: plan.content },
261
+ };
262
+ }
176
263
  export function advanceHead(db, projectId, baseRevisionId, revision) {
177
264
  const result = db
178
265
  .prepare("UPDATE project_heads SET revision_id = ? WHERE project_id = ? AND revision_id = ?")
179
266
  .run(revision.id, projectId, baseRevisionId);
180
267
  if (Number(result.changes) !== 1)
181
268
  return false;
269
+ // The project's channel follows its settings: moved in Edit project, or restored to a
270
+ // version from another channel. A channel since deleted leaves the project where it is.
271
+ const channelId = revision.config.channelId;
272
+ if (channelId !== undefined &&
273
+ resolveChannelId(db, channelId) === channelId &&
274
+ projectChannelId(db, projectId) !== channelId)
275
+ setProjectChannel(db, projectId, channelId);
182
276
  db.prepare("UPDATE projects SET title = ?, format = ?, config = ?, updated_at = ? WHERE id = ?").run(revision.config.title, revision.config.format, JSON.stringify(revision.config), revision.createdAt, projectId);
183
277
  return true;
184
278
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { stageKinds } from "../../kernel/pipeline.js";
3
3
  import { pieceKinds, pieceStates } from "../../kernel/runner/piece-repo.js";
4
4
  import { runConfigSchema } from "../admission/schema.js";
5
+ import { scaledImagesMax } from "../images/scale.js";
5
6
  import { outputSchema } from "../storage/schema.js";
6
7
  const id = z
7
8
  .string()
@@ -16,6 +17,13 @@ const cue = z
16
17
  text: z.string().trim().min(1).max(10000),
17
18
  start: z.number().finite().nonnegative(),
18
19
  end: z.number().finite().positive(),
20
+ // The speaker's id on a multi-voice run (`voices/model.ts`); absent everywhere else.
21
+ speaker: z
22
+ .string()
23
+ .min(1)
24
+ .max(60)
25
+ .regex(/^[a-z0-9-]+$/)
26
+ .optional(),
19
27
  })
20
28
  .strict()
21
29
  .refine((value) => value.end > value.start, {
@@ -35,7 +43,8 @@ export const revisionContentSchema = z
35
43
  reference: id.optional(),
36
44
  })
37
45
  .strict(),
38
- imageOrder: z.array(id).max(60),
46
+ // The most any project may hold; `rebuild/recipe-validation.ts` holds each to its own cap.
47
+ imageOrder: z.array(id).max(scaledImagesMax),
39
48
  imageDefinitions: z.record(id, z
40
49
  .object({
41
50
  source: z.enum(["generate", "provide"]),
@@ -79,6 +88,7 @@ export const revisionContentSchema = z
79
88
  })
80
89
  .strict())
81
90
  .optional(),
91
+ ambientBed: id.optional(),
82
92
  })
83
93
  .strict();
84
94
  export const revisionEditSchema = z
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import { causedBy } from "../../kernel/errors.js";
3
+ // Living within a CLI's plan limits. When a CLI says its allowance is used up, the time it
4
+ // returns is stored per account; every call to that CLI - from any project, and after a
5
+ // restart - waits for it instead of failing. Other providers are untouched, so a project
6
+ // whose narration is keyed carries on while its Codex images wait.
7
+ // Waking a little after the stated reset, so the first call is not refused again at the edge.
8
+ export const resetMarginMs = 2 * 60_000;
9
+ // When the CLI names no reset time, the wait checks again this often.
10
+ export const recheckMs = 30 * 60_000;
11
+ const waitRow = z.object({ retry_at: z.string() });
12
+ export function createLimitGate(deps) {
13
+ // How many calls of one stage wait on one account: a stage drawing four Codex images at
14
+ // once shows one wait, removed when the last of them wakes.
15
+ const waiting = new Map();
16
+ const retryAt = (account) => {
17
+ const row = deps.db
18
+ .prepare("SELECT retry_at FROM plan_limit_waits WHERE account = ?")
19
+ .get(account);
20
+ if (row === undefined)
21
+ return undefined;
22
+ return Date.parse(waitRow.parse(row).retry_at);
23
+ };
24
+ const begin = (account, waiter) => {
25
+ const key = JSON.stringify([waiter.projectId, waiter.stage, account]);
26
+ waiting.set(key, (waiting.get(key) ?? 0) + 1);
27
+ const inserted = deps.db
28
+ .prepare("INSERT OR IGNORE INTO plan_limit_waiters (project_id, stage, account, since) VALUES (?, ?, ?, ?)")
29
+ .run(waiter.projectId, waiter.stage, account, deps.clock.now().toISOString());
30
+ if (inserted.changes === 1)
31
+ deps.changed(waiter.projectId);
32
+ };
33
+ // Only a wait that ran its course removes the row. One cut short by cancel, pause or the
34
+ // app stopping leaves it, and the next start decides what it means (resumeAfterRestart).
35
+ const end = (account, waiter, woke) => {
36
+ const key = JSON.stringify([waiter.projectId, waiter.stage, account]);
37
+ const left = (waiting.get(key) ?? 1) - 1;
38
+ if (left > 0) {
39
+ waiting.set(key, left);
40
+ return;
41
+ }
42
+ waiting.delete(key);
43
+ if (!woke)
44
+ return;
45
+ deps.db
46
+ .prepare("DELETE FROM plan_limit_waiters WHERE project_id = ? AND stage = ? AND account = ?")
47
+ .run(waiter.projectId, waiter.stage, account);
48
+ deps.changed(waiter.projectId);
49
+ };
50
+ return {
51
+ ready: async (account, waiter, signal) => {
52
+ for (;;) {
53
+ signal.throwIfAborted();
54
+ const due = retryAt(account);
55
+ if (due === undefined)
56
+ return;
57
+ const left = due - deps.clock.now().getTime();
58
+ if (left <= 0) {
59
+ // Past the reset: the allowance is back. A call refused again stores a new wait.
60
+ deps.db
61
+ .prepare("DELETE FROM plan_limit_waits WHERE account = ? AND retry_at <= ?")
62
+ .run(account, deps.clock.now().toISOString());
63
+ return;
64
+ }
65
+ begin(account, waiter);
66
+ let woke = false;
67
+ try {
68
+ await deps.clock.sleep(left, signal);
69
+ woke = true;
70
+ }
71
+ finally {
72
+ end(account, waiter, woke);
73
+ }
74
+ }
75
+ },
76
+ exhausted: (hit) => {
77
+ const now = deps.clock.now().getTime();
78
+ const reset = hit.resetsAt === null ? Number.NaN : Date.parse(hit.resetsAt);
79
+ // A stated reset already past (a clock skew, a stale message) is checked again soon.
80
+ const due = Number.isFinite(reset) && reset > now ? reset + resetMarginMs : now + recheckMs;
81
+ const retry = new Date(due).toISOString();
82
+ try {
83
+ deps.db
84
+ .prepare(`INSERT INTO plan_limit_waits (account, resets_at, retry_at, detected_at) VALUES (?, ?, ?, ?)
85
+ ON CONFLICT(account) DO UPDATE SET resets_at = excluded.resets_at, retry_at = excluded.retry_at, detected_at = excluded.detected_at
86
+ WHERE excluded.retry_at > plan_limit_waits.retry_at`)
87
+ .run(hit.account, Number.isFinite(reset) && reset > now ? new Date(reset).toISOString() : null, retry, new Date(now).toISOString());
88
+ }
89
+ catch (error) {
90
+ deps.log.write("error", "plan-limits", { detail: causedBy(error) });
91
+ throw error;
92
+ }
93
+ deps.log.write("info", "plan-limits", {
94
+ detail: `${hit.account} plan limit used up; waiting until ${retry}`,
95
+ });
96
+ },
97
+ };
98
+ }
99
+ // At start, a stage that was waiting on a plan limit when the app stopped was marked
100
+ // interrupted like any other (main.ts markInterruptedStages). Those projects are resumed so
101
+ // the wait carries on by itself - their calls meet the stored reset and wait again. A project
102
+ // paused or cancelled meanwhile is left as it is.
103
+ export async function resumeAfterRestart(db, resume, log) {
104
+ const projects = db
105
+ .prepare(`SELECT DISTINCT w.project_id AS id FROM plan_limit_waiters w
106
+ JOIN stages s ON s.project_id = w.project_id AND s.kind = w.stage
107
+ WHERE s.state = 'failed' AND s.failure_reason = 'interrupted'
108
+ AND NOT EXISTS (SELECT 1 FROM project_controls c WHERE c.project_id = w.project_id AND c.paused = 1)`)
109
+ .all()
110
+ .map((row) => z.string().parse(row.id));
111
+ db.prepare("DELETE FROM plan_limit_waiters").run();
112
+ let resumed = 0;
113
+ for (const projectId of projects) {
114
+ try {
115
+ if (await resume(projectId))
116
+ resumed += 1;
117
+ else
118
+ log.write("warn", "plan-limits", {
119
+ projectId,
120
+ detail: "A project waiting for CLI limits could not be resumed after the restart; use Continue the run on the project page.",
121
+ });
122
+ }
123
+ catch (error) {
124
+ log.write("error", "plan-limits", { projectId, detail: causedBy(error) });
125
+ }
126
+ }
127
+ return resumed;
128
+ }