@gentbajko/slopify 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +11 -1
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -1,5 +1,7 @@
1
1
  import { statSync } from "node:fs";
2
2
  import { z } from "zod";
3
+ import { referenceKey } from "../admission/model.js";
4
+ import { usesReference } from "../admission/rules.js";
3
5
  import { outputPath, stagingPath } from "../storage/layout.js";
4
6
  import { stagedFileById } from "../storage/repo.js";
5
7
  export function bindUpload(content, upload, assetId) {
@@ -31,7 +33,7 @@ export function validateUploads(deps, edit) {
31
33
  for (const [index, upload] of (edit.uploads ?? []).entries()) {
32
34
  const field = `uploads.${index}`;
33
35
  const to = upload.destination;
34
- if ((to.kind === "provided" && edit.config.sources[to.stage] !== "provide") ||
36
+ if ((to.kind === "provided" && !providesOwnFile(edit.config, to.stage)) ||
35
37
  (to.kind === "narration" && edit.config.sources.audio !== "generate") ||
36
38
  (to.kind === "image" && edit.config.sources.images === "off"))
37
39
  fields.push({
@@ -52,7 +54,13 @@ export function validateUploads(deps, edit) {
52
54
  staged.add(upload.stagedFileId);
53
55
  destinations.add(destination);
54
56
  const file = stagedFileById(deps.db, upload.stagedFileId);
55
- const kind = to.kind === "provided" ? to.stage : to.kind === "image" ? "images" : "audio";
57
+ const kind = to.kind === "provided"
58
+ ? to.stage === "reference"
59
+ ? "images"
60
+ : to.stage
61
+ : to.kind === "image"
62
+ ? "images"
63
+ : "audio";
56
64
  if (file === undefined ||
57
65
  file.state !== "staged" ||
58
66
  file.stageKind !== kind ||
@@ -77,7 +85,7 @@ export function validateUploads(deps, edit) {
77
85
  export function validateAssetReferences(deps, projectId, content, prepared) {
78
86
  const fields = [];
79
87
  const refs = [];
80
- for (const key of ["research", "article", "audio", "thumbnail"]) {
88
+ for (const key of ["research", "article", "audio", "thumbnail", "reference"]) {
81
89
  const id = content.provided[key];
82
90
  if (id !== undefined)
83
91
  refs.push([`content.provided.${key}`, id, key, false]);
@@ -94,7 +102,10 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
94
102
  audio: ["audio_body", "audio_intro", "audio_outro", "audio_export"],
95
103
  images: ["image"],
96
104
  thumbnail: ["thumbnail"],
105
+ reference: ["reference"],
97
106
  };
107
+ // The establishing image is the Images stage's.
108
+ const stageOf = (kind) => (kind === "reference" ? "images" : kind);
98
109
  for (const [field, id, kind, allowPiece] of refs) {
99
110
  if (!prepared.some((row) => row.id === id && row.projectId === projectId) &&
100
111
  deps.db
@@ -107,7 +118,7 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
107
118
  if (!prepared.some((row) => row.id === id) &&
108
119
  deps.db
109
120
  .prepare("SELECT 1 FROM revision_outputs WHERE project_id=? AND asset_id=? AND json_extract(descriptor,'$.stageKind')=? AND json_extract(descriptor,'$.role') IN (SELECT value FROM json_each(?))")
110
- .get(projectId, id, kind, JSON.stringify(roles[kind])) === undefined &&
121
+ .get(projectId, id, stageOf(kind), JSON.stringify(roles[kind])) === undefined &&
111
122
  (!allowPiece ||
112
123
  deps.db
113
124
  .prepare("SELECT 1 FROM revision_pieces WHERE project_id=? AND asset_id=? AND stage_kind=? AND json_extract(descriptor,'$.kind') IN (SELECT value FROM json_each(?))")
@@ -133,6 +144,16 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
133
144
  field: "content.shortsMusic",
134
145
  message: "This music file does not belong to this project. Reload the page and choose the music again.",
135
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
+ });
136
157
  return fields;
137
158
  }
138
159
  export async function measureAudio(deps, projectId, path) {
@@ -150,12 +171,25 @@ export function assetPath(deps, projectId, id) {
150
171
  .prepare("SELECT path FROM project_assets WHERE project_id=? AND id=?")
151
172
  .get(projectId, id)).path;
152
173
  }
174
+ // A stage whose own file the project uses: Provide for audio and the thumbnail, Upload for the
175
+ // establishing image.
176
+ export function providesOwnFile(config, kind) {
177
+ return kind === "reference"
178
+ ? usesReference(config) && config.reference?.source === "provide"
179
+ : config.sources[kind] === "provide";
180
+ }
181
+ // Where each provided file is published: its work key and output role.
182
+ export const providedOutput = {
183
+ audio: { workKey: "audio:provided", role: "audio_body" },
184
+ thumbnail: { workKey: "thumbnail:image", role: "thumbnail" },
185
+ reference: { workKey: referenceKey, role: "reference" },
186
+ };
153
187
  export function providedAssetSelected(base, kind, assetId) {
154
- return (base.revision.config.sources[kind] === "provide" &&
188
+ return (providesOwnFile(base.revision.config, kind) &&
155
189
  base.outputs.some((row) => row.selected &&
156
190
  row.assetId === assetId &&
157
- row.workKey === (kind === "audio" ? "audio:provided" : "thumbnail:image") &&
158
- row.output.role === (kind === "audio" ? "audio_body" : "thumbnail")));
191
+ row.workKey === providedOutput[kind].workKey &&
192
+ row.output.role === providedOutput[kind].role));
159
193
  }
160
194
  export function validateReplacementAvailability(deps, base, edit) {
161
195
  const previous = new Set([
@@ -163,10 +197,10 @@ export function validateReplacementAvailability(deps, base, edit) {
163
197
  ...Object.values(base.revision.content.imageDefinitions).map((row) => row.assetId),
164
198
  ...Object.values(base.revision.content.narrationOverrides).flatMap((row) => row.kind === "asset" ? [row.assetId] : []),
165
199
  ]);
166
- for (const kind of ["audio", "thumbnail"]) {
200
+ for (const kind of ["audio", "thumbnail", "reference"]) {
167
201
  const assetId = edit.content.provided[kind];
168
202
  if (assetId !== undefined &&
169
- edit.config.sources[kind] === "provide" &&
203
+ providesOwnFile(edit.config, kind) &&
170
204
  !providedAssetSelected(base, kind, assetId) &&
171
205
  !edit.uploads?.some((row) => row.destination.kind === "provided" && row.destination.stage === kind))
172
206
  previous.delete(assetId);
@@ -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$/, ":");
@@ -5,7 +5,7 @@ import { normalizeArticleIntent } from "../rebuild/recipe-save.js";
5
5
  import { discardPreparedAssets } from "../storage/assets.js";
6
6
  import { prepareStagedFile, prepareText } from "../storage/prepare.js";
7
7
  import { outputSchema } from "../storage/schema.js";
8
- import { bindUpload, measureAudio, providedAssetSelected, } from "./mutation-assets.js";
8
+ import { bindUpload, measureAudio, providedAssetSelected, providedOutput, providesOwnFile, } from "./mutation-assets.js";
9
9
  export async function prepareEditAssets(deps, base, edit) {
10
10
  const prepared = [];
11
11
  const allocated = [];
@@ -16,8 +16,8 @@ export async function prepareEditAssets(deps, base, edit) {
16
16
  const to = upload.destination;
17
17
  const role = to.kind === "image"
18
18
  ? "image"
19
- : to.kind === "provided" && to.stage === "thumbnail"
20
- ? "thumbnail"
19
+ : to.kind === "provided" && to.stage !== "audio"
20
+ ? providedOutput[to.stage].role
21
21
  : "audio_body";
22
22
  const result = prepareStagedFile(deps, {
23
23
  projectId,
@@ -35,9 +35,7 @@ export async function prepareEditAssets(deps, base, edit) {
35
35
  ? to.key
36
36
  : to.kind === "shortsMusic"
37
37
  ? "shorts:music"
38
- : to.stage === "audio"
39
- ? "audio:provided"
40
- : "thumbnail:image";
38
+ : providedOutput[to.stage].workKey;
41
39
  const item = {
42
40
  ...result,
43
41
  upload,
@@ -93,21 +91,21 @@ export async function prepareEditAssets(deps, base, edit) {
93
91
  slot: workKey,
94
92
  });
95
93
  }
96
- for (const kind of ["audio", "thumbnail"]) {
94
+ for (const kind of ["audio", "thumbnail", "reference"]) {
97
95
  const assetId = content.provided[kind];
98
- if (edit.config.sources[kind] !== "provide" ||
96
+ if (!providesOwnFile(edit.config, kind) ||
99
97
  assetId === undefined ||
100
98
  providedAssetSelected(base, kind, assetId) ||
101
99
  prepared.some((row) => row.asset.id === assetId))
102
100
  continue;
103
101
  const existing = deps.db
104
102
  .prepare("SELECT descriptor FROM revision_outputs WHERE project_id=? AND asset_id=? AND json_extract(descriptor,'$.stageKind')=? LIMIT 1")
105
- .get(projectId, assetId, kind);
103
+ .get(projectId, assetId, kind === "reference" ? "images" : kind);
106
104
  if (existing === undefined)
107
105
  throw new Error("Validated replacement asset has no descriptor.");
108
106
  const descriptor = outputSchema.parse(JSON.parse(z.string().parse(existing.descriptor)));
109
107
  const asset = retainedAsset(deps, projectId, assetId);
110
- const role = kind === "audio" ? "audio_body" : "thumbnail";
108
+ const role = providedOutput[kind].role;
111
109
  const item = {
112
110
  asset,
113
111
  output: {
@@ -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, {
@@ -32,9 +40,11 @@ export const revisionContentSchema = z
32
40
  article: id.optional(),
33
41
  audio: id.optional(),
34
42
  thumbnail: id.optional(),
43
+ reference: id.optional(),
35
44
  })
36
45
  .strict(),
37
- 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),
38
48
  imageDefinitions: z.record(id, z
39
49
  .object({
40
50
  source: z.enum(["generate", "provide"]),
@@ -78,6 +88,7 @@ export const revisionContentSchema = z
78
88
  })
79
89
  .strict())
80
90
  .optional(),
91
+ ambientBed: id.optional(),
81
92
  })
82
93
  .strict();
83
94
  export const revisionEditSchema = z
@@ -91,7 +102,10 @@ export const revisionEditSchema = z
91
102
  stagedFileId: id,
92
103
  destination: z.discriminatedUnion("kind", [
93
104
  z
94
- .object({ kind: z.literal("provided"), stage: z.enum(["audio", "thumbnail"]) })
105
+ .object({
106
+ kind: z.literal("provided"),
107
+ stage: z.enum(["audio", "thumbnail", "reference"]),
108
+ })
95
109
  .strict(),
96
110
  z.object({ kind: z.literal("image"), imageKey: id }).strict(),
97
111
  z.object({ kind: z.literal("narration"), key: workKey }).strict(),
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ import { planAccountOf } from "../../kernel/ports/plan-limits.js";
2
+ import { resolveChannelId, scheduleChannel } from "../channels/repo.js";
3
+ import { priceCall } from "./pricing.js";
4
+ // Prices each call as it lands and keeps it with the rates it used; a CLI's plan windows go
5
+ // beside it. Separate from the anonymous telemetry log: this is per project, never leaves the
6
+ // machine, and is kept whether or not the usage-stats notice was dismissed.
7
+ export function createUsageMeter(deps) {
8
+ return {
9
+ record: (call) => {
10
+ const priced = priceCall(call, deps.catalogue());
11
+ const at = deps.clock.now().toISOString();
12
+ deps.db
13
+ .prepare(`INSERT INTO provider_usage (id, project_id, stage, kind, provider, model, tokens_in, tokens_out, tokens_cached, characters, images, seconds, size, quality, wall_ms, on_plan, cost, api_model, api_cost, price_json, created_at)
14
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
15
+ .run(deps.ids.next(), call.projectId, call.stage, call.kind, call.provider, call.model, call.tokensIn ?? null, call.tokensOut ?? null, call.cachedTokens ?? null, call.characters ?? null, call.images ?? null, call.seconds ?? null, call.size ?? null, call.quality ?? null, Math.round(call.wallMs), priced.onPlan ? 1 : 0, priced.cost, priced.apiModel, priced.apiCost, priced.price === null ? null : JSON.stringify(priced.price), at);
16
+ const account = planAccountOf(call.provider);
17
+ if (account !== undefined && call.limits !== undefined)
18
+ deps.db
19
+ .prepare("INSERT INTO plan_limit_readings (id, project_id, account, reading_json, created_at) VALUES (?, ?, ?, ?, ?)")
20
+ .run(deps.ids.next(), call.projectId, account, JSON.stringify(call.limits), at);
21
+ },
22
+ };
23
+ }
24
+ // The same pricing for a call made outside a project (`kernel/runner/standalone.ts`), kept
25
+ // against its schedule or channel with the channel it counts under on Home.
26
+ export function createStandaloneMeter(deps) {
27
+ return {
28
+ record: (call) => {
29
+ const priced = priceCall(call, deps.catalogue());
30
+ const account = planAccountOf(call.provider);
31
+ const reading = account !== undefined && call.limits !== undefined
32
+ ? { account, json: JSON.stringify(call.limits) }
33
+ : undefined;
34
+ deps.db
35
+ .prepare(`INSERT INTO standalone_usage (id, owner_kind, owner_id, channel_id, purpose, kind, provider, model, tokens_in, tokens_out, tokens_cached, characters, images, seconds, size, quality, wall_ms, on_plan, cost, api_model, api_cost, price_json, account, reading_json, created_at)
36
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
37
+ .run(deps.ids.next(), call.owner.kind, call.owner.id, channelOf(deps.db, call.owner), call.purpose, call.kind, call.provider, call.model, call.tokensIn ?? null, call.tokensOut ?? null, call.cachedTokens ?? null, call.characters ?? null, call.images ?? null, call.seconds ?? null, call.size ?? null, call.quality ?? null, Math.round(call.wallMs), priced.onPlan ? 1 : 0, priced.cost, priced.apiModel, priced.apiCost, priced.price === null ? null : JSON.stringify(priced.price), reading?.account ?? null, reading?.json ?? null, deps.clock.now().toISOString());
38
+ },
39
+ };
40
+ }
41
+ // A schedule counts under its template's channel; one whose template is gone, like a channel
42
+ // this install doesn't have, under the default channel.
43
+ function channelOf(db, owner) {
44
+ return owner.kind === "schedule"
45
+ ? resolveChannelId(db, scheduleChannel(db, owner.id)?.id)
46
+ : resolveChannelId(db, owner.id);
47
+ }