@gentbajko/slopify 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +3 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/inworld-async.js +9 -5
  44. package/dist/adapters/tts/inworld.js +17 -3
  45. package/dist/adapters/tts/openai.js +3 -0
  46. package/dist/adapters/tts/voice-languages.js +80 -0
  47. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  48. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  49. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/OFL.txt +93 -0
  53. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  54. package/dist/assets/sample/sample-audiobook.tar +0 -0
  55. package/dist/assets/sample/sample-podcast.tar +0 -0
  56. package/dist/assets/sample/sample-project.tar +0 -0
  57. package/dist/catalog/merge.js +132 -0
  58. package/dist/catalog/registry.js +13 -13
  59. package/dist/catalog/schema.js +2 -0
  60. package/dist/catalog/store.js +147 -8
  61. package/dist/compose.yaml +71 -0
  62. package/dist/edge/autostart/docker-record.js +32 -0
  63. package/dist/edge/autostart/docker.js +50 -0
  64. package/dist/edge/autostart/index.js +78 -0
  65. package/dist/edge/autostart/launcher.js +176 -0
  66. package/dist/edge/autostart/model.js +3 -0
  67. package/dist/edge/autostart/native.js +210 -0
  68. package/dist/edge/autostart/prompt.js +66 -0
  69. package/dist/edge/autostart/service.js +81 -0
  70. package/dist/edge/cli.js +57 -51
  71. package/dist/edge/docker-install/activation.js +56 -0
  72. package/dist/edge/docker-install/apply.js +458 -0
  73. package/dist/edge/docker-install/engine.js +362 -0
  74. package/dist/edge/docker-install/run.js +154 -0
  75. package/dist/edge/docker-install/state.js +146 -0
  76. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  77. package/dist/edge/docker.js +14 -22
  78. package/dist/edge/host-cli.js +1 -1
  79. package/dist/edge/http/actions.js +32 -10
  80. package/dist/edge/http/app.js +58 -2
  81. package/dist/edge/http/auditions.js +70 -0
  82. package/dist/edge/http/autostart.js +37 -0
  83. package/dist/edge/http/channel-memory.js +92 -0
  84. package/dist/edge/http/channels.js +128 -0
  85. package/dist/edge/http/entries.js +22 -3
  86. package/dist/edge/http/files.js +1 -1
  87. package/dist/edge/http/folder-location.js +1 -1
  88. package/dist/edge/http/home.js +39 -0
  89. package/dist/edge/http/host-cli.js +29 -5
  90. package/dist/edge/http/narration-peaks.js +76 -0
  91. package/dist/edge/http/onboarding.js +152 -0
  92. package/dist/edge/http/open-folder.js +1 -1
  93. package/dist/edge/http/patch-notes.js +45 -0
  94. package/dist/edge/http/planning.js +4 -2
  95. package/dist/edge/http/project-create.js +87 -0
  96. package/dist/edge/http/projects.js +31 -99
  97. package/dist/edge/http/prompts.js +40 -3
  98. package/dist/edge/http/pronunciations.js +23 -4
  99. package/dist/edge/http/providers.js +86 -0
  100. package/dist/edge/http/reviews.js +121 -0
  101. package/dist/edge/http/revision-files.js +1 -1
  102. package/dist/edge/http/revisions.js +1 -1
  103. package/dist/edge/http/run-cost.js +25 -0
  104. package/dist/edge/http/schedules.js +111 -6
  105. package/dist/edge/http/settings.js +84 -6
  106. package/dist/edge/http/storage-files.js +50 -0
  107. package/dist/edge/http/storage.js +29 -2
  108. package/dist/edge/http/studio.js +207 -0
  109. package/dist/edge/http/style-preview.js +100 -0
  110. package/dist/edge/http/subtitles.js +1 -1
  111. package/dist/edge/http/trash.js +69 -0
  112. package/dist/edge/http/update.js +24 -3
  113. package/dist/edge/http/whats-new.js +15 -0
  114. package/dist/edge/http/youtube-edits.js +83 -0
  115. package/dist/edge/native-update.js +62 -0
  116. package/dist/host-cli/open-folder.js +10 -10
  117. package/dist/host-cli/runtime.js +9 -1
  118. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  119. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  120. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  121. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  122. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  123. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  124. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  125. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  126. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  127. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  128. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  129. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  130. package/dist/kernel/paths.js +27 -6
  131. package/dist/kernel/pipeline.js +4 -1
  132. package/dist/kernel/ports/host-cli.js +122 -14
  133. package/dist/kernel/ports/key-probe.js +1 -0
  134. package/dist/kernel/ports/languages.js +157 -0
  135. package/dist/kernel/ports/model.js +5 -2
  136. package/dist/kernel/ports/narration-aliases.js +60 -0
  137. package/dist/kernel/ports/plan-limits.js +36 -0
  138. package/dist/kernel/runner/attempt.js +17 -8
  139. package/dist/kernel/runner/audition.js +36 -0
  140. package/dist/kernel/runner/graph.js +24 -5
  141. package/dist/kernel/runner/index.js +35 -5
  142. package/dist/kernel/runner/meter.js +1 -0
  143. package/dist/kernel/runner/providers.js +142 -7
  144. package/dist/kernel/runner/retry-policy.js +43 -0
  145. package/dist/kernel/runner/standalone.js +134 -0
  146. package/dist/kernel/runner/work-authority.js +28 -2
  147. package/dist/main.js +272 -16
  148. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  149. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  150. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  151. package/dist/patch-notes/3.0.0.md +70 -0
  152. package/dist/patch-notes/index.json +26 -0
  153. package/dist/slices/admission/model.js +27 -0
  154. package/dist/slices/admission/repo.js +24 -4
  155. package/dist/slices/admission/rules.js +79 -1
  156. package/dist/slices/admission/schema.js +58 -1
  157. package/dist/slices/admission/short-mode.js +47 -0
  158. package/dist/slices/admission/start.js +41 -5
  159. package/dist/slices/article/continuation.js +2 -2
  160. package/dist/slices/article/provided-entries.js +2 -2
  161. package/dist/slices/article/run.js +6 -2
  162. package/dist/slices/article/segments.js +3 -3
  163. package/dist/slices/backups/folder.js +14 -6
  164. package/dist/slices/batch/index.js +7 -2
  165. package/dist/slices/cancel/index.js +1 -1
  166. package/dist/slices/channels/cast-images.js +110 -0
  167. package/dist/slices/channels/cast-match.js +32 -0
  168. package/dist/slices/channels/images.js +19 -0
  169. package/dist/slices/channels/model.js +5 -0
  170. package/dist/slices/channels/rebrand.js +100 -0
  171. package/dist/slices/channels/repo.js +218 -0
  172. package/dist/slices/channels/runs.js +129 -0
  173. package/dist/slices/channels/schema.js +129 -0
  174. package/dist/slices/channels/service.js +180 -0
  175. package/dist/slices/channels/studio-csv.js +110 -0
  176. package/dist/slices/channels/videos.js +120 -0
  177. package/dist/slices/checkpoints/recovery.js +10 -0
  178. package/dist/slices/control/index.js +2 -1
  179. package/dist/slices/document/fonts.js +1 -1
  180. package/dist/slices/document/render.js +1 -1
  181. package/dist/slices/episodes/related.js +90 -0
  182. package/dist/slices/episodes/repo.js +60 -0
  183. package/dist/slices/episodes/service.js +58 -0
  184. package/dist/slices/episodes/summarize.js +170 -0
  185. package/dist/slices/estimate/index.js +109 -28
  186. package/dist/slices/estimate/requests.js +60 -2
  187. package/dist/slices/fixes/rules.js +73 -0
  188. package/dist/slices/fonts/coverage.js +82 -0
  189. package/dist/slices/fonts/sfnt.js +74 -0
  190. package/dist/slices/images/run.js +8 -5
  191. package/dist/slices/images/scale.js +84 -0
  192. package/dist/slices/library/diff.js +122 -0
  193. package/dist/slices/library/history.js +100 -0
  194. package/dist/slices/library/model.js +3 -0
  195. package/dist/slices/library/photorealistic.js +49 -0
  196. package/dist/slices/library/repo.js +19 -14
  197. package/dist/slices/library/save.js +62 -12
  198. package/dist/slices/library/slots.js +7 -1
  199. package/dist/slices/library/used-by.js +153 -0
  200. package/dist/slices/loudness/level-pieces.js +41 -0
  201. package/dist/slices/loudness/loudnorm.js +164 -0
  202. package/dist/slices/loudness/model.js +153 -0
  203. package/dist/slices/model-upkeep/model.js +10 -0
  204. package/dist/slices/model-upkeep/switch.js +217 -0
  205. package/dist/slices/model-upkeep/usage.js +183 -0
  206. package/dist/slices/narration/aliases-library.js +31 -0
  207. package/dist/slices/narration/aliases-schema.js +53 -0
  208. package/dist/slices/narration/aliases.js +49 -0
  209. package/dist/slices/narration/blocks.js +352 -0
  210. package/dist/slices/narration/chunk.js +19 -7
  211. package/dist/slices/narration/concat.js +2 -2
  212. package/dist/slices/narration/describe.js +45 -0
  213. package/dist/slices/narration/pauses-model.js +63 -0
  214. package/dist/slices/narration/pauses.js +269 -0
  215. package/dist/slices/narration/peaks-model.js +1 -0
  216. package/dist/slices/narration/peaks.js +94 -0
  217. package/dist/slices/narration/preparation.js +21 -3
  218. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  219. package/dist/slices/narration/pronunciation.js +72 -25
  220. package/dist/slices/narration/run.js +6 -6
  221. package/dist/slices/narration/shared-glossary.js +1 -1
  222. package/dist/slices/narration/steering.js +15 -4
  223. package/dist/slices/notifications/notifier.js +21 -3
  224. package/dist/slices/notifications/rules.js +22 -1
  225. package/dist/slices/onboarding/copy.js +143 -0
  226. package/dist/slices/onboarding/first-run.js +46 -0
  227. package/dist/slices/onboarding/install.js +119 -0
  228. package/dist/slices/onboarding/model.js +64 -0
  229. package/dist/slices/onboarding/packs.js +491 -0
  230. package/dist/slices/onboarding/quick-short.js +129 -0
  231. package/dist/slices/onboarding/sample.js +132 -0
  232. package/dist/slices/onboarding/state.js +68 -0
  233. package/dist/slices/onboarding/template.js +73 -0
  234. package/dist/slices/patch-notes/library.js +73 -0
  235. package/dist/slices/patch-notes/seen.js +77 -0
  236. package/dist/slices/play-drafts/convert.js +125 -6
  237. package/dist/slices/play-drafts/readiness.js +3 -0
  238. package/dist/slices/play-drafts/repo.js +2 -0
  239. package/dist/slices/play-drafts/review-inputs.js +20 -3
  240. package/dist/slices/play-drafts/schema.js +89 -0
  241. package/dist/slices/play-drafts/service.js +4 -1
  242. package/dist/slices/play-drafts/start.js +43 -18
  243. package/dist/slices/project-templates/from-project.js +50 -4
  244. package/dist/slices/project-templates/one-off.js +26 -0
  245. package/dist/slices/project-templates/repo.js +2 -1
  246. package/dist/slices/project-templates/service.js +15 -4
  247. package/dist/slices/project-templates/setup.js +18 -2
  248. package/dist/slices/rebuild/admission-repo.js +1 -1
  249. package/dist/slices/rebuild/model.js +6 -0
  250. package/dist/slices/rebuild/preview-details.js +46 -3
  251. package/dist/slices/rebuild/preview-plan.js +13 -6
  252. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  253. package/dist/slices/rebuild/recipe-audio.js +66 -12
  254. package/dist/slices/rebuild/recipe-build.js +8 -4
  255. package/dist/slices/rebuild/recipe-cast.js +18 -0
  256. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  257. package/dist/slices/rebuild/recipe-describe.js +93 -0
  258. package/dist/slices/rebuild/recipe-edit.js +13 -1
  259. package/dist/slices/rebuild/recipe-exports.js +62 -8
  260. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  261. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  262. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  263. package/dist/slices/rebuild/recipe-model.js +25 -6
  264. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  265. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  266. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  267. package/dist/slices/rebuild/recipe-reference.js +15 -1
  268. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  269. package/dist/slices/rebuild/recipe-save.js +4 -1
  270. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  271. package/dist/slices/rebuild/recipe-text.js +114 -19
  272. package/dist/slices/rebuild/recipe-validation.js +14 -2
  273. package/dist/slices/rebuild/recipe-visual.js +96 -33
  274. package/dist/slices/rebuild/recipe-voices.js +286 -0
  275. package/dist/slices/rebuild/recipe-work.js +77 -6
  276. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  277. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  278. package/dist/slices/rebuild/recovery-model.js +3 -0
  279. package/dist/slices/rebuild/recovery-selection.js +28 -2
  280. package/dist/slices/rebuild/recovery.js +35 -17
  281. package/dist/slices/rebuild/repo.js +3 -3
  282. package/dist/slices/rebuild/retry.js +18 -0
  283. package/dist/slices/rebuild/review-redo.js +110 -0
  284. package/dist/slices/rebuild/runtime-actions.js +5 -0
  285. package/dist/slices/rebuild/runtime-admission.js +4 -1
  286. package/dist/slices/rebuild/runtime-animate.js +4 -4
  287. package/dist/slices/rebuild/runtime-article.js +5 -5
  288. package/dist/slices/rebuild/runtime-document.js +4 -4
  289. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  290. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  291. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  292. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  293. package/dist/slices/rebuild/runtime-export.js +84 -19
  294. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  295. package/dist/slices/rebuild/runtime-image.js +21 -2
  296. package/dist/slices/rebuild/runtime-local.js +148 -24
  297. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  298. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  299. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  300. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  301. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  302. package/dist/slices/rebuild/runtime-provider.js +126 -18
  303. package/dist/slices/rebuild/runtime-publication.js +21 -5
  304. package/dist/slices/rebuild/runtime-review.js +237 -0
  305. package/dist/slices/rebuild/runtime-run.js +17 -11
  306. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  307. package/dist/slices/rebuild/runtime-store.js +39 -10
  308. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  309. package/dist/slices/rebuild/runtime-voices.js +103 -0
  310. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  311. package/dist/slices/rebuild/soften.js +49 -0
  312. package/dist/slices/rebuild/transition-repo.js +9 -2
  313. package/dist/slices/research/run.js +4 -4
  314. package/dist/slices/research/synthesis.js +2 -2
  315. package/dist/slices/reviews/model.js +105 -0
  316. package/dist/slices/reviews/outcome.js +22 -0
  317. package/dist/slices/reviews/repo.js +106 -0
  318. package/dist/slices/reviews/rules.js +63 -0
  319. package/dist/slices/reviews/verdict.js +71 -0
  320. package/dist/slices/reviews/view.js +30 -0
  321. package/dist/slices/revisions/adopt-content.js +5 -2
  322. package/dist/slices/revisions/adopt.js +3 -1
  323. package/dist/slices/revisions/downloads.js +7 -1
  324. package/dist/slices/revisions/image-plan.js +183 -0
  325. package/dist/slices/revisions/mutation-assets.js +10 -0
  326. package/dist/slices/revisions/mutation-cues.js +71 -0
  327. package/dist/slices/revisions/mutation-request.js +2 -1
  328. package/dist/slices/revisions/mutations.js +97 -3
  329. package/dist/slices/revisions/schema.js +11 -1
  330. package/dist/slices/run-cost/limits.js +128 -0
  331. package/dist/slices/run-cost/meter.js +47 -0
  332. package/dist/slices/run-cost/panel.js +234 -0
  333. package/dist/slices/run-cost/pricing.js +92 -0
  334. package/dist/slices/run-cost/week.js +108 -0
  335. package/dist/slices/schedules/agenda.js +158 -0
  336. package/dist/slices/schedules/repo.js +58 -18
  337. package/dist/slices/schedules/scheduler.js +67 -16
  338. package/dist/slices/schedules/schema.js +138 -1
  339. package/dist/slices/schedules/service.js +43 -4
  340. package/dist/slices/schedules/similar.js +90 -0
  341. package/dist/slices/schedules/topic-list.js +163 -0
  342. package/dist/slices/schedules/topics.js +422 -0
  343. package/dist/slices/settings/first-run.js +102 -0
  344. package/dist/slices/settings/health.js +190 -0
  345. package/dist/slices/settings/key-guides.js +128 -0
  346. package/dist/slices/settings/key-test.js +85 -0
  347. package/dist/slices/settings/model.js +6 -1
  348. package/dist/slices/settings/playback.js +15 -0
  349. package/dist/slices/settings/repo.js +17 -3
  350. package/dist/slices/settings/voices.js +64 -4
  351. package/dist/slices/settings/whats-new.js +64 -0
  352. package/dist/slices/shorts/captions.js +9 -0
  353. package/dist/slices/shorts/pick.js +3 -3
  354. package/dist/slices/shorts/prompts.js +1 -1
  355. package/dist/slices/shorts/render.js +54 -5
  356. package/dist/slices/storage/asset-name.js +3 -0
  357. package/dist/slices/storage/backup-export.js +48 -9
  358. package/dist/slices/storage/backup-format.js +38 -1
  359. package/dist/slices/storage/backup-import.js +191 -19
  360. package/dist/slices/storage/delete-project.js +7 -4
  361. package/dist/slices/storage/documents.js +79 -0
  362. package/dist/slices/storage/files-location.js +531 -0
  363. package/dist/slices/storage/layout.js +14 -4
  364. package/dist/slices/storage/model.js +11 -0
  365. package/dist/slices/storage/portable.js +92 -12
  366. package/dist/slices/storage/reconcile.js +14 -1
  367. package/dist/slices/storage/repo.js +4 -1
  368. package/dist/slices/storage/schema.js +11 -0
  369. package/dist/slices/storage/trim.js +146 -0
  370. package/dist/slices/studio/disclosure.js +74 -0
  371. package/dist/slices/studio/model.js +28 -0
  372. package/dist/slices/studio/pack.js +209 -0
  373. package/dist/slices/studio/settings.js +133 -0
  374. package/dist/slices/style-preview/images.js +61 -0
  375. package/dist/slices/style-preview/render.js +161 -0
  376. package/dist/slices/style-preview/schema.js +45 -0
  377. package/dist/slices/style-preview/service.js +93 -0
  378. package/dist/slices/style-preview/settings.js +89 -0
  379. package/dist/slices/subtitles/captions.js +42 -9
  380. package/dist/slices/subtitles/model.js +22 -0
  381. package/dist/slices/subtitles/prepare.js +27 -16
  382. package/dist/slices/subtitles/transcript.js +1 -1
  383. package/dist/slices/thumbnail/run.js +7 -7
  384. package/dist/slices/trash/model.js +25 -0
  385. package/dist/slices/trash/service.js +273 -0
  386. package/dist/slices/uploads/repo.js +20 -0
  387. package/dist/slices/video/ambient-bed-schema.js +15 -0
  388. package/dist/slices/video/ambient-bed.js +95 -0
  389. package/dist/slices/video/ambient-mix.js +72 -0
  390. package/dist/slices/video/audio-inputs.js +2 -2
  391. package/dist/slices/video/cards.js +5 -2
  392. package/dist/slices/video/edit-list.js +23 -2
  393. package/dist/slices/video/edit-settings.js +13 -3
  394. package/dist/slices/video/ffmpeg.js +63 -9
  395. package/dist/slices/video/figure-card.js +492 -0
  396. package/dist/slices/video/figure-spans.js +51 -0
  397. package/dist/slices/video/plan.js +125 -13
  398. package/dist/slices/video/run.js +14 -12
  399. package/dist/slices/video/slideshow.js +39 -4
  400. package/dist/slices/voices/attribution.js +69 -0
  401. package/dist/slices/voices/audio-files.js +82 -0
  402. package/dist/slices/voices/audition.js +25 -0
  403. package/dist/slices/voices/cast.js +56 -0
  404. package/dist/slices/voices/grouping.js +49 -0
  405. package/dist/slices/voices/join.js +35 -0
  406. package/dist/slices/voices/languages.js +25 -0
  407. package/dist/slices/voices/model.js +179 -0
  408. package/dist/slices/voices/palette.js +26 -0
  409. package/dist/slices/voices/panel.js +91 -0
  410. package/dist/slices/voices/portraits.js +55 -0
  411. package/dist/slices/voices/script.js +136 -0
  412. package/dist/slices/voices/timing.js +58 -0
  413. package/dist/slices/youtube/answer.js +1 -1
  414. package/dist/slices/youtube/chapters.js +126 -0
  415. package/dist/slices/youtube/edits-repo.js +69 -0
  416. package/dist/slices/youtube/edits.js +89 -0
  417. package/dist/slices/youtube/placeholders.js +98 -0
  418. package/dist/slices/youtube/transcript.js +11 -4
  419. package/dist/updater/service.js +66 -21
  420. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  421. package/dist/web/assets/index-CX590kE-.js +303 -0
  422. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  423. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  424. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  425. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  426. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  427. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  428. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  429. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  430. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  431. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
  432. package/dist/web/index.html +2 -2
  433. package/package.json +4 -4
  434. package/dist/edge/docker-launch.js +0 -81
  435. package/dist/edge/docker-projects/activation.js +0 -35
  436. package/dist/edge/docker-projects/claims.js +0 -85
  437. package/dist/edge/docker-projects/committed.js +0 -40
  438. package/dist/edge/docker-projects/engine.js +0 -461
  439. package/dist/edge/docker-projects/install.js +0 -254
  440. package/dist/edge/docker-projects/prune.js +0 -66
  441. package/dist/edge/docker-projects/recover.js +0 -100
  442. package/dist/edge/docker-projects/state.js +0 -225
  443. package/dist/web/assets/index-CktpxlJd.js +0 -171
  444. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  445. package/scripts/docker-run.sh +0 -3
  446. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,273 @@
1
+ import { z } from "zod";
2
+ import { transact } from "../../kernel/db/tx.js";
3
+ import { deleteVersions, recordVersion } from "../library/history.js";
4
+ import { nameMax } from "../library/model.js";
5
+ import { entryById, promptById } from "../library/repo.js";
6
+ import { deleteProject, projectBusy } from "../storage/delete-project.js";
7
+ import { trashKinds, trashMs, } from "./model.js";
8
+ // Settings → Trash: what was deleted in the last 30 days, put back or removed for good.
9
+ //
10
+ // Each kind keeps its stamp where it lives: a project in `project_trash` (the projects table is
11
+ // read positionally in many places), a prompt, entry or template in its own `deleted_at`, and a
12
+ // schedule in the `deleted_at` it always had as the tombstone that keeps its run history. A
13
+ // schedule leaves the trash by getting `purged_at`; its row and history stay, as before.
14
+ const dayMs = 24 * 60 * 60_000;
15
+ // A template's name column allows this much (0008's CHECK on project_template_revisions).
16
+ const templateNameMax = 200;
17
+ const row = z.object({
18
+ id: z.string(),
19
+ name: z.string(),
20
+ detail: z.string().nullable(),
21
+ deleted_at: z.string(),
22
+ });
23
+ // Every kind in one statement, newest deletion first.
24
+ const trashedRows = `
25
+ SELECT 'project' AS kind, p.id, p.title AS name, NULL AS detail, t.deleted_at
26
+ FROM project_trash t JOIN projects p ON p.id = t.project_id
27
+ UNION ALL SELECT 'prompt', id, name, kind, deleted_at FROM prompts WHERE deleted_at IS NOT NULL
28
+ UNION ALL SELECT 'entry', id, name, category, deleted_at FROM entries WHERE deleted_at IS NOT NULL
29
+ UNION ALL SELECT 'template', t.id, r.name, NULL, t.deleted_at
30
+ FROM project_templates t
31
+ JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
32
+ WHERE t.deleted_at IS NOT NULL
33
+ UNION ALL SELECT 'schedule', id, name, NULL, deleted_at FROM schedules
34
+ WHERE deleted_at IS NOT NULL AND purged_at IS NULL`;
35
+ export function listTrash(deps) {
36
+ const now = deps.clock.now().valueOf();
37
+ return deps.db
38
+ .prepare(`SELECT * FROM (${trashedRows}) ORDER BY deleted_at DESC, kind, id`)
39
+ .all()
40
+ .map((raw) => {
41
+ const parsed = row.extend({ kind: z.enum(trashKinds) }).parse(raw);
42
+ const purgeAt = Date.parse(parsed.deleted_at) + trashMs;
43
+ return {
44
+ kind: parsed.kind,
45
+ id: parsed.id,
46
+ name: parsed.name,
47
+ detail: parsed.detail,
48
+ deletedAt: parsed.deleted_at,
49
+ purgeAt: new Date(purgeAt).toISOString(),
50
+ daysLeft: Math.max(0, Math.ceil((purgeAt - now) / dayMs)),
51
+ };
52
+ });
53
+ }
54
+ // 07 Projects' Delete. Refused while the run is going, as removing it always was: the work
55
+ // has to be stopped by the user first. A project waiting its turn (a queued batch item, a
56
+ // checkpoint, work left over from a restart) simply stops being picked up: the runner holds a
57
+ // project in the trash like a paused one and the batch queue moves on past it.
58
+ export function trashProject(deps, projectId) {
59
+ return transact(deps.db, () => {
60
+ const found = deps.db
61
+ .prepare("SELECT (SELECT 1 FROM project_trash t WHERE t.project_id = p.id) AS trashed FROM projects p WHERE p.id = ?")
62
+ .get(projectId);
63
+ if (found === undefined || found.trashed === 1)
64
+ return { ok: false, reason: "not-found" };
65
+ if (projectBusy(deps, projectId))
66
+ return { ok: false, reason: "running" };
67
+ deps.db
68
+ .prepare("INSERT INTO project_trash (project_id, deleted_at) VALUES (?, ?)")
69
+ .run(projectId, deps.clock.now().toISOString());
70
+ return { ok: true, value: null };
71
+ });
72
+ }
73
+ // Puts the item back where it was. A prompt, entry or template whose name a live one has
74
+ // taken meanwhile comes back as "Name (restored)", then "Name (restored 2)"…
75
+ //
76
+ // A schedule comes back paused with no next run, whatever it was when deleted (only a
77
+ // canceled or completed one can be deleted): nothing runs until Resume is pressed on
78
+ // Schedules, which picks the next occurrence from then. Its template must be live: one in the
79
+ // trash is restored first, and one removed for good leaves the schedule nothing to run.
80
+ export function restoreItem(deps, kind, id) {
81
+ return transact(deps.db, () => {
82
+ switch (kind) {
83
+ case "project":
84
+ return restoreProject(deps.db, id);
85
+ case "prompt":
86
+ return restoreLibrary(deps, "prompt", id);
87
+ case "entry":
88
+ return restoreLibrary(deps, "entry", id);
89
+ case "template":
90
+ return restoreTemplate(deps, id);
91
+ case "schedule":
92
+ return restoreSchedule(deps, id);
93
+ }
94
+ });
95
+ }
96
+ function restoreProject(db, id) {
97
+ const found = db
98
+ .prepare("SELECT p.title FROM project_trash t JOIN projects p ON p.id = t.project_id WHERE t.project_id = ?")
99
+ .get(id);
100
+ if (typeof found?.title !== "string")
101
+ return { ok: false, reason: "not-found" };
102
+ db.prepare("DELETE FROM project_trash WHERE project_id = ?").run(id);
103
+ return { ok: true, value: { kind: "project", id, name: found.title, renamedFrom: null } };
104
+ }
105
+ function restoreLibrary(deps, itemKind, id) {
106
+ const table = itemKind === "prompt" ? "prompts" : "entries";
107
+ const group = itemKind === "prompt" ? "kind" : "category";
108
+ const found = deps.db
109
+ .prepare(`SELECT name, ${group} AS grp FROM ${table} WHERE id = ? AND deleted_at IS NOT NULL`)
110
+ .get(id);
111
+ if (typeof found?.name !== "string" || typeof found.grp !== "string")
112
+ return { ok: false, reason: "not-found" };
113
+ const taken = deps.db.prepare(`SELECT 1 FROM ${table} WHERE ${group} = ? AND lower(name) = lower(?) AND deleted_at IS NULL`);
114
+ const grp = found.grp;
115
+ const name = freeName(found.name, nameMax, (candidate) => taken.get(grp, candidate) !== undefined);
116
+ const renamed = name !== found.name;
117
+ deps.db
118
+ .prepare(`UPDATE ${table} SET deleted_at = NULL, name = ?, updated_at = ${renamed ? "?" : "updated_at"} WHERE id = ?`)
119
+ .run(...(renamed ? [name, deps.clock.now().toISOString(), id] : [name, id]));
120
+ // A rename shows in History as a version of its own.
121
+ if (renamed) {
122
+ const item = itemKind === "prompt" ? promptById(deps.db, id) : entryById(deps.db, id);
123
+ if (item !== undefined)
124
+ recordVersion(deps.db, itemKind, item);
125
+ }
126
+ return {
127
+ ok: true,
128
+ value: { kind: itemKind, id, name, renamedFrom: renamed ? found.name : null },
129
+ };
130
+ }
131
+ function restoreTemplate(deps, id) {
132
+ const found = deps.db
133
+ .prepare(`SELECT r.name, r.version, r.document_json FROM project_templates t
134
+ JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
135
+ WHERE t.id = ? AND t.deleted_at IS NOT NULL`)
136
+ .get(id);
137
+ if (typeof found?.name !== "string" ||
138
+ typeof found.version !== "number" ||
139
+ typeof found.document_json !== "string")
140
+ return { ok: false, reason: "not-found" };
141
+ const taken = deps.db.prepare(`SELECT 1 FROM project_templates t
142
+ JOIN project_template_revisions r ON r.template_id = t.id AND r.version = t.head_version
143
+ WHERE t.deleted_at IS NULL AND lower(r.name) = lower(?)`);
144
+ const name = freeName(found.name, templateNameMax, (candidate) => taken.get(candidate) !== undefined);
145
+ deps.db.prepare("UPDATE project_templates SET deleted_at = NULL WHERE id = ?").run(id);
146
+ // A rename is a new version, as a rename in the template editor is; the versions a schedule
147
+ // or a draft names stay as they were.
148
+ if (name !== found.name) {
149
+ const version = found.version + 1;
150
+ deps.db
151
+ .prepare("INSERT INTO project_template_revisions(template_id,version,name,document_json,created_at) VALUES (?,?,?,?,?)")
152
+ .run(id, version, name, found.document_json, deps.clock.now().toISOString());
153
+ deps.db.prepare("UPDATE project_templates SET head_version = ? WHERE id = ?").run(version, id);
154
+ }
155
+ return {
156
+ ok: true,
157
+ value: {
158
+ kind: "template",
159
+ id,
160
+ name,
161
+ renamedFrom: name === found.name ? null : found.name,
162
+ },
163
+ };
164
+ }
165
+ function restoreSchedule(deps, id) {
166
+ const found = deps.db
167
+ .prepare(`SELECT s.name, s.template_id,
168
+ (SELECT t.deleted_at IS NOT NULL FROM project_templates t WHERE t.id = s.template_id) AS trashed
169
+ FROM schedules s WHERE s.id = ? AND s.deleted_at IS NOT NULL AND s.purged_at IS NULL`)
170
+ .get(id);
171
+ if (typeof found?.name !== "string")
172
+ return { ok: false, reason: "not-found" };
173
+ if (found.trashed === null)
174
+ return { ok: false, reason: "template-gone" };
175
+ if (found.trashed === 1)
176
+ return { ok: false, reason: "template-in-trash" };
177
+ const at = deps.clock.now().toISOString();
178
+ deps.db
179
+ .prepare(`UPDATE schedules SET deleted_at = NULL, status = 'paused', next_run_at = NULL,
180
+ version = version + 1, updated_at = ? WHERE id = ?`)
181
+ .run(at, id);
182
+ return { ok: true, value: { kind: "schedule", id, name: found.name, renamedFrom: null } };
183
+ }
184
+ // "Delete now": removes one item for good. A project's folder goes first, then its rows
185
+ // (`slices/storage/delete-project.ts`); a folder that will not go leaves it in the trash.
186
+ export function deleteNow(deps, kind, id) {
187
+ switch (kind) {
188
+ case "project": {
189
+ if (deps.db.prepare("SELECT 1 FROM project_trash WHERE project_id = ?").get(id) === undefined)
190
+ return { ok: false, reason: "not-found" };
191
+ const removed = deleteProject(deps, id);
192
+ if (removed.ok)
193
+ return { ok: true, value: null };
194
+ return {
195
+ ok: false,
196
+ reason: removed.reason === "no-project" ? "not-found" : removed.reason,
197
+ ...(removed.detail === undefined ? {} : { detail: removed.detail }),
198
+ };
199
+ }
200
+ case "prompt":
201
+ case "entry":
202
+ return transact(deps.db, () => {
203
+ const table = kind === "prompt" ? "prompts" : "entries";
204
+ const changed = deps.db
205
+ .prepare(`DELETE FROM ${table} WHERE id = ? AND deleted_at IS NOT NULL`)
206
+ .run(id).changes;
207
+ if (Number(changed) === 0)
208
+ return { ok: false, reason: "not-found" };
209
+ deleteVersions(deps.db, kind, id);
210
+ return { ok: true, value: null };
211
+ });
212
+ case "template": {
213
+ // Revisions cascade. A schedule in the trash may still name it: that schedule can then
214
+ // no longer be restored (`restoreSchedule`), only removed.
215
+ const changed = deps.db
216
+ .prepare("DELETE FROM project_templates WHERE id = ? AND deleted_at IS NOT NULL")
217
+ .run(id).changes;
218
+ return Number(changed) === 0 ? { ok: false, reason: "not-found" } : { ok: true, value: null };
219
+ }
220
+ case "schedule": {
221
+ // Gone from the trash; the row stays as the history Schedules lists under Deleted.
222
+ const changed = deps.db
223
+ .prepare("UPDATE schedules SET purged_at = ? WHERE id = ? AND deleted_at IS NOT NULL AND purged_at IS NULL")
224
+ .run(deps.clock.now().toISOString(), id).changes;
225
+ return Number(changed) === 0 ? { ok: false, reason: "not-found" } : { ok: true, value: null };
226
+ }
227
+ }
228
+ }
229
+ // The daily purge: everything deleted more than 30 days ago goes for good. A failure on one
230
+ // item is logged and leaves it for the next pass; the rest still go.
231
+ export function purgeExpired(deps) {
232
+ const cutoff = new Date(deps.clock.now().valueOf() - trashMs).toISOString();
233
+ let removed = 0;
234
+ let kept = 0;
235
+ for (const item of listTrash(deps)) {
236
+ if (item.deletedAt > cutoff)
237
+ continue;
238
+ const result = deleteNow(deps, item.kind, item.id);
239
+ if (result.ok)
240
+ removed++;
241
+ else {
242
+ kept++;
243
+ deps.log.write("warn", "trash.purge", {
244
+ ...(item.kind === "project" ? { projectId: item.id } : {}),
245
+ detail: `${item.kind} ${item.id}: ${result.detail ?? result.reason}`,
246
+ });
247
+ }
248
+ }
249
+ return { removed, kept };
250
+ }
251
+ export function createTrashPurge(deps) {
252
+ let last;
253
+ return {
254
+ tick: () => {
255
+ const now = deps.clock.now().valueOf();
256
+ if (last !== undefined && now - last < dayMs)
257
+ return undefined;
258
+ last = now;
259
+ return purgeExpired(deps);
260
+ },
261
+ };
262
+ }
263
+ // The name itself when free, else "Name (restored)", "Name (restored 2)"… cut to fit `max`.
264
+ export function freeName(name, max, taken) {
265
+ if (!taken(name))
266
+ return name;
267
+ for (let n = 1;; n++) {
268
+ const suffix = n === 1 ? " (restored)" : ` (restored ${String(n)})`;
269
+ const candidate = `${name.slice(0, Math.max(1, max - suffix.length)).trimEnd()}${suffix}`;
270
+ if (!taken(candidate))
271
+ return candidate;
272
+ }
273
+ }
@@ -0,0 +1,20 @@
1
+ export function markUploaded(db, projectId, uploaded, at) {
2
+ if (db.prepare("SELECT 1 FROM projects WHERE id=?").get(projectId) === undefined)
3
+ return { ok: false, reason: "not-found" };
4
+ if (!uploaded) {
5
+ db.prepare("DELETE FROM project_uploads WHERE project_id=?").run(projectId);
6
+ return { ok: true, uploadedAt: null };
7
+ }
8
+ // Marking twice keeps the first time: it is when the video went up.
9
+ db.prepare("INSERT INTO project_uploads(project_id,uploaded_at) VALUES (?,?) ON CONFLICT(project_id) DO NOTHING").run(projectId, at);
10
+ const row = db
11
+ .prepare("SELECT uploaded_at FROM project_uploads WHERE project_id=?")
12
+ .get(projectId);
13
+ return { ok: true, uploadedAt: typeof row?.uploaded_at === "string" ? row.uploaded_at : at };
14
+ }
15
+ export function uploadedProjects(db) {
16
+ return new Map(db
17
+ .prepare("SELECT project_id, uploaded_at FROM project_uploads")
18
+ .all()
19
+ .map((row) => [String(row.project_id), String(row.uploaded_at)]));
20
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { ambientBedSources, builtInBeds } from "./ambient-bed.js";
3
+ // The ambient bed as a run's config and a channel's brand kit hold it. The ranges are
4
+ // `ambient-bed.ts`'s, checked by admission and the channel page, not the schema's.
5
+ const numbers = {
6
+ levelDb: z.number(),
7
+ fadeInSeconds: z.number(),
8
+ tailSeconds: z.number(),
9
+ };
10
+ export const ambientBedSchema = z
11
+ .object({ source: z.enum(ambientBedSources), ...numbers })
12
+ .strict();
13
+ export const channelAmbientBedSchema = z
14
+ .object({ source: z.enum(builtInBeds), ...numbers })
15
+ .strict();
@@ -0,0 +1,95 @@
1
+ // An ambient sound bed under the long video: rain, fire or wind that ffmpeg makes itself from
2
+ // noise (no downloads, no bundled recordings), or the user's own audio file. It plays under the
3
+ // whole narration, ducked whenever the voice speaks, fades in at the start, and keeps playing
4
+ // for a fade-out tail after the narration ends, which lengthens the video when the tail is
5
+ // longer than the silence already there (`plan.ts`). Only the long video gets it: the shorts
6
+ // have their own background music, and the audio-only WAV export stays the narration alone.
7
+ // Browser-safe: Play, the channel page and the review summary read these names and limits too.
8
+ //
9
+ // A run without one has no `ambientBed` at all, and the render recipe leaves it out of the
10
+ // fingerprint then, so no video made before it existed turns outdated.
11
+ export const builtInBeds = ["rain", "fire", "wind"];
12
+ export const ambientBedSources = [...builtInBeds, "upload"];
13
+ export const ambientBedLabels = {
14
+ rain: "Rain",
15
+ fire: "Fireplace",
16
+ wind: "Wind",
17
+ upload: "My own file",
18
+ };
19
+ // ceiling: below -40 dB the bed is inaudible under a voice; above -6 dB it competes with it.
20
+ export const ambientLevelMin = -40;
21
+ export const ambientLevelMax = -6;
22
+ export const ambientFadeMax = 30;
23
+ export const ambientTailMax = 30;
24
+ export const defaultAmbientBed = {
25
+ levelDb: -18,
26
+ fadeInSeconds: 3,
27
+ tailSeconds: 6,
28
+ };
29
+ // Whether the run's long video gets the bed: it is set, the video renders and there is a
30
+ // narration to lie under. A short-only run (`admission/short-mode.ts`) renders through the
31
+ // Shorts renderer and has no bed.
32
+ export function usesAmbientBed(config) {
33
+ return (config.ambientBed !== undefined &&
34
+ config.mode !== "short" &&
35
+ config.sources.video === "generate" &&
36
+ config.sources.audio !== "off");
37
+ }
38
+ // The words a problem with one of the settings is said in; shared by admission, Play's draft
39
+ // conversion and the channel page.
40
+ export function ambientBedProblems(bed) {
41
+ const problems = [];
42
+ if (!ambientBedSources.includes(bed.source))
43
+ problems.push({
44
+ field: "source",
45
+ message: "Choose Rain, Fireplace, Wind or My own file for the ambient sound.",
46
+ });
47
+ if (!(Number.isInteger(bed.levelDb) &&
48
+ bed.levelDb >= ambientLevelMin &&
49
+ bed.levelDb <= ambientLevelMax))
50
+ problems.push({
51
+ field: "level",
52
+ message: `Enter an ambient sound level between ${String(ambientLevelMin)} and ${String(ambientLevelMax)} dB, in whole dB.`,
53
+ });
54
+ if (!halfSteps(bed.fadeInSeconds, ambientFadeMax))
55
+ problems.push({
56
+ field: "fadeIn",
57
+ message: `Enter a fade-in between 0 and ${String(ambientFadeMax)} seconds, in steps of 0.5.`,
58
+ });
59
+ if (!halfSteps(bed.tailSeconds, ambientTailMax))
60
+ problems.push({
61
+ field: "tail",
62
+ message: `Enter a tail between 0 and ${String(ambientTailMax)} seconds, in steps of 0.5.`,
63
+ });
64
+ return problems;
65
+ }
66
+ function halfSteps(value, max) {
67
+ return Number.isInteger(value * 2) && value >= 0 && value <= max;
68
+ }
69
+ // How much longer the video runs with the bed: the tail plays over the silence already after
70
+ // the narration (`edgeSilenceSeconds`) and only the rest is added.
71
+ export function ambientTailExtension(config, edgeSilenceSeconds) {
72
+ if (!usesAmbientBed(config) || config.ambientBed === undefined)
73
+ return 0;
74
+ return Math.max(0, config.ambientBed.tailSeconds - edgeSilenceSeconds);
75
+ }
76
+ export function ambientBedFormOf(bed) {
77
+ return {
78
+ source: bed.source,
79
+ level: String(bed.levelDb),
80
+ fadeIn: String(bed.fadeInSeconds),
81
+ tail: String(bed.tailSeconds),
82
+ };
83
+ }
84
+ // The settings a form asks for, numbers as typed (NaN for a blank one), or undefined for none.
85
+ export function ambientBedOfForm(form) {
86
+ if (form === undefined || form.source === "none")
87
+ return undefined;
88
+ const number = (raw) => (raw.trim() === "" ? Number.NaN : Number(raw));
89
+ return {
90
+ source: form.source,
91
+ levelDb: number(form.level),
92
+ fadeInSeconds: number(form.fadeIn),
93
+ tailSeconds: number(form.tail),
94
+ };
95
+ }
@@ -0,0 +1,72 @@
1
+ // How the join mixes the ambient bed under the narration (`ffmpeg.ts`). Every value goes into
2
+ // an argument array, never a shell string.
3
+ //
4
+ // The built-in beds are shaped noise, made by ffmpeg's own anoisesrc with a fixed seed, so the
5
+ // same settings always render the same sound and nothing is downloaded or shipped:
6
+ // - rain: pink noise with the rumble and the fizz filtered off, a steady patter;
7
+ // - wind: brown noise kept low, swelling and dropping in slow gusts;
8
+ // - fire: a low brown-noise roar that flickers, with sparse velvet-noise crackle over it.
9
+ // Each is levelled to about -20 dBFS RMS before the bed's own level, so 0 dB is roughly as loud
10
+ // as a narration and the three sit alike at the same setting.
11
+ //
12
+ // The ducking is a sidechain compressor keyed by the narration itself, the Shorts' music does
13
+ // the same (`shorts/render.ts`): it follows the voice as it is, needs no word timing (which a
14
+ // run without captions, chapters or shorts never makes), and dips under an intro or outro too.
15
+ // ceiling: it dips once the voice passes about -34 dBFS, by up to 8:1, over 40 ms, and comes
16
+ // back over 900 ms after the voice stops: slower than the shorts' music, so a bed under a long
17
+ // narration settles rather than swelling in every pause between sentences.
18
+ const duck = "threshold=0.02:ratio=8:attack=40:release=900:makeup=1";
19
+ const presets = {
20
+ rain: [{ source: "anoisesrc=c=pink:a=1:seed=17", filters: "highpass=f=600,lowpass=f=9000" }],
21
+ wind: [
22
+ {
23
+ source: "anoisesrc=c=brown:a=1:seed=29",
24
+ filters: "highpass=f=60,lowpass=f=500,tremolo=f=0.15:d=0.6,volume=-1dB",
25
+ },
26
+ ],
27
+ fire: [
28
+ {
29
+ source: "anoisesrc=c=brown:a=1:seed=41",
30
+ filters: "highpass=f=80,lowpass=f=800,tremolo=f=6:d=0.25,volume=-3dB",
31
+ },
32
+ {
33
+ source: "anoisesrc=c=velvet:a=1:seed=43",
34
+ filters: "highpass=f=1500,lowpass=f=6000,volume=-22dB",
35
+ },
36
+ ],
37
+ };
38
+ // The inputs the bed adds after the join's own, for `seconds` of video.
39
+ export function bedInputs(bed, seconds, sampleRate) {
40
+ if (bed.source.kind === "file")
41
+ // Looped endlessly and cut to the video, so a file shorter than the video plays again.
42
+ return ["-stream_loop", "-1", "-i", bed.source.path];
43
+ return presets[bed.source.preset].flatMap((layer) => [
44
+ "-f",
45
+ "lavfi",
46
+ "-t",
47
+ seconds.toFixed(6),
48
+ "-i",
49
+ `${layer.source}:r=${String(sampleRate)}`,
50
+ ]);
51
+ }
52
+ // The chains that turn the join's narration `voice` into `out` with the bed under it; `first`
53
+ // is the index of the bed's first input.
54
+ export function bedChains(bed, first, seconds, format, voice, out) {
55
+ const chains = [];
56
+ const layers = bed.source.kind === "file" ? [{ filters: "" }] : presets[bed.source.preset];
57
+ layers.forEach((layer, at) => {
58
+ chains.push(`[${String(first + at)}:a]${layer.filters === "" ? "" : `${layer.filters},`}${format},atrim=end=${seconds.toFixed(6)},asetpts=PTS-STARTPTS[bed${String(at)}]`);
59
+ });
60
+ const joined = layers.length === 1
61
+ ? "[bed0]anull"
62
+ : `${layers.map((_layer, at) => `[bed${String(at)}]`).join("")}amix=inputs=${String(layers.length)}:duration=first:normalize=0`;
63
+ const fadeIn = bed.fadeInSeconds > 0 ? `,afade=t=in:st=0:d=${bed.fadeInSeconds.toFixed(3)}` : "";
64
+ // A tail of 0 still fades, over a moment, rather than cutting off mid-sound.
65
+ const fadeOut = `,afade=t=out:st=${bed.fadeOutAt.toFixed(6)}:d=${Math.max(0.25, bed.fadeOutSeconds).toFixed(3)}`;
66
+ chains.push(`${joined},volume=${String(bed.levelDb)}dB${fadeIn}${fadeOut}[bed]`);
67
+ chains.push(`${voice}asplit=2[voice][key]`);
68
+ chains.push(`[bed][key]sidechaincompress=${duck}[ducked]`);
69
+ // The narration keeps its level; `normalize=0` stops amix halving both.
70
+ chains.push(`[voice][ducked]amix=inputs=2:duration=first:dropout_transition=0:normalize=0${out}`);
71
+ return chains;
72
+ }
@@ -5,9 +5,9 @@ export async function audioInputs(deps, projectId, outputs, signal) {
5
5
  const dir = projectDir(deps.paths, projectId);
6
6
  const body = await audio(deps, outputs, "audio_body", dir, signal);
7
7
  if (!body)
8
- throw new Error("The video has no narration audio to play. Let the Narration stage finish, then Retry stage.");
8
+ throw new Error("The video has no narration audio to play. Let the Narration stage finish, then Try again.");
9
9
  if (body.seconds <= 0)
10
- throw new Error("The narration audio contains no sound; the file may be damaged or empty. Regenerate the narration (or upload a different audio file in Edit project → Narration), then Retry stage.");
10
+ throw new Error("The narration audio contains no sound; the file may be damaged or empty. Regenerate the narration (or upload a different audio file in Edit project → Narration), then Try again.");
11
11
  const intro = await audio(deps, outputs, "audio_intro", dir, signal);
12
12
  const outro = await audio(deps, outputs, "audio_outro", dir, signal);
13
13
  return { body, intro, outro };
@@ -1,3 +1,4 @@
1
+ import { assColour } from "../subtitles/captions.js";
1
2
  // Chapter cards as an ASS script the clips draw with libass, the renderer the captions use,
2
3
  // so a card is set in the subtitle font without a font-rendering filter of its own (this
3
4
  // ffmpeg has none). Each card is centred, well above where captions sit by default, and fades
@@ -5,7 +6,9 @@
5
6
  // clip's first frame (`offset`), since a clip's frames count from zero.
6
7
  // ceiling: the owner's eye. The fades take 0.4 s each of the card's 2.5 s.
7
8
  const fadeMs = 400;
8
- export function cardsAss(edit, cards, fontName, offset = 0) {
9
+ export function cardsAss(edit, cards, fontName, offset = 0,
10
+ // #RRGGBB from the brand kit; absent is white.
11
+ color) {
9
12
  const { width, height, fps } = edit;
10
13
  // The title is sized from the frame's short side, so a vertical video's card fits too. At
11
14
  // 8% it read as a caption rather than a chapter title; 11% and bold gives it presence.
@@ -24,7 +27,7 @@ export function cardsAss(edit, cards, fontName, offset = 0) {
24
27
  "[V4+ Styles]",
25
28
  "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
26
29
  // White bold text, the soft outline, no offset shadow, centred (alignment 5), letter-spaced.
27
- `Style: Card,${fontName.replace(/,/g, " ")},${String(size)},&H00FFFFFF,&H00FFFFFF,&H60000000,&H00000000,-1,0,0,0,100,100,${String(Math.round(size / 12))},0,1,${String(outline)},0,5,${String(Math.round(width * 0.1))},${String(Math.round(width * 0.1))},0,1`,
30
+ `Style: Card,${fontName.replace(/,/g, " ")},${String(size)},${assColour(color, "&H00FFFFFF")},${assColour(color, "&H00FFFFFF")},&H60000000,&H00000000,-1,0,0,0,100,100,${String(Math.round(size / 12))},0,1,${String(outline)},0,5,${String(Math.round(width * 0.1))},${String(Math.round(width * 0.1))},0,1`,
28
31
  "",
29
32
  "[Events]",
30
33
  "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { builtInBeds } from "./ambient-bed.js";
2
3
  import { atmospheres, colorGrades, lookLevels, } from "./edit-settings.js";
3
4
  // The edit list is the whole video as data: what plays, for how many frames, how it moves,
4
5
  // and the audio under it. Planning (`plan.ts`) writes one and the renderer (`ffmpeg.ts`,
@@ -28,6 +29,9 @@ export function withPaths(edit, map) {
28
29
  ...(edit.cardFont === undefined
29
30
  ? {}
30
31
  : { cardFont: { ...edit.cardFont, path: map(edit.cardFont.path) } }),
32
+ ...(edit.bed?.source.kind === "file"
33
+ ? { bed: { ...edit.bed, source: { kind: "file", path: map(edit.bed.source.path) } } }
34
+ : {}),
31
35
  };
32
36
  }
33
37
  const share = z.number().min(0).max(1);
@@ -88,6 +92,23 @@ const editListSchema = z
88
92
  .object({ path: z.string().min(1), name: z.string().min(1) })
89
93
  .strict()
90
94
  .optional(),
95
+ cardColor: z
96
+ .string()
97
+ .regex(/^#[0-9A-Fa-f]{6}$/)
98
+ .optional(),
99
+ bed: z
100
+ .object({
101
+ source: z.discriminatedUnion("kind", [
102
+ z.object({ kind: z.literal("noise"), preset: z.enum(builtInBeds) }).strict(),
103
+ z.object({ kind: z.literal("file"), path: z.string().min(1) }).strict(),
104
+ ]),
105
+ levelDb: z.number(),
106
+ fadeInSeconds: z.number().nonnegative(),
107
+ fadeOutAt: z.number().nonnegative(),
108
+ fadeOutSeconds: z.number().nonnegative(),
109
+ })
110
+ .strict()
111
+ .optional(),
91
112
  })
92
113
  .strict();
93
114
  // Reads a recorded edit list back. Strict, so a list written by a newer Slopify, with a
@@ -95,9 +116,9 @@ const editListSchema = z
95
116
  export function readEditList(value) {
96
117
  const version = typeof value === "object" && value !== null && "version" in value ? value.version : undefined;
97
118
  if (version !== editListVersion)
98
- throw new Error(`This video's edit list is version ${String(version)}, and this Slopify reads version ${editListVersion}. Update Slopify, or use Re-run section on Video to plan it again.`);
119
+ throw new Error(`This video's edit list is version ${String(version)}, and this Slopify reads version ${editListVersion}. Update Slopify, or use More → Render the video again in the Video section to plan it again.`);
99
120
  const parsed = editListSchema.safeParse(value);
100
121
  if (!parsed.success)
101
- throw new Error(`This video's edit list is damaged (${parsed.error.issues[0]?.path.join(".") || "the list"}: ${parsed.error.issues[0]?.message ?? "not readable"}). Use Re-run section on Video to plan it again.`);
122
+ throw new Error(`This video's edit list is damaged (${parsed.error.issues[0]?.path.join(".") || "the list"}: ${parsed.error.issues[0]?.message ?? "not readable"}). Use More → Render the video again in the Video section to plan it again.`);
102
123
  return parsed.data;
103
124
  }
@@ -1,3 +1,4 @@
1
+ import { wordTimingUnavailable } from "../../kernel/ports/languages.js";
1
2
  // How the Video stage edits the slideshow beyond seconds per image, zoom and motion: where the
2
3
  // cuts fall, how one shot hands over to the next, the Look (vignette, grain, colour, an
3
4
  // atmosphere overlay and chapter cards), and which images come to life as short clips.
@@ -49,6 +50,8 @@ export const animatePrompt = "Subtle cinematic motion: a slow, steady camera pus
49
50
  export const cutFloorShare = 0.4;
50
51
  // How long a chapter card stays on screen, fades included.
51
52
  export const chapterCardSeconds = 2.5;
53
+ // How long the brand kit's end screen card stays on screen at the end of the video.
54
+ export const endScreenSeconds = 5;
52
55
  export const cutModeLabels = {
53
56
  interval: "Every N seconds",
54
57
  narration: "Follow the narration",
@@ -87,12 +90,14 @@ export const animateModeLabels = {
87
90
  export function videoEditOf(config) {
88
91
  return config.videoEdit ?? legacyVideoEdit;
89
92
  }
90
- // Following the narration needs narration to follow and a video to cut; otherwise the video
91
- // falls back to Every N seconds, which is also what its fingerprint says.
93
+ // Following the narration needs narration to follow, a video to cut and word timing in the
94
+ // project language (`wordTimingUnavailable`); otherwise the video falls back to Every N
95
+ // seconds, which is also what its fingerprint says.
92
96
  export function usesNarrationCuts(config) {
93
97
  return (videoEditOf(config).cuts === "narration" &&
94
98
  config.sources.audio !== "off" &&
95
- config.sources.video === "generate");
99
+ config.sources.video === "generate" &&
100
+ wordTimingUnavailable(config.language) === undefined);
96
101
  }
97
102
  // Chapter cards are placed from the narration's word timing, so they need narration too.
98
103
  export function usesChapterCards(config) {
@@ -111,6 +116,11 @@ export function usesAnimation(config) {
111
116
  // know which images open a chapter.
112
117
  export function editNeedsTiming(config) {
113
118
  return (usesNarrationCuts(config) ||
119
+ // "Show tables and figures on screen" places each card where its description is spoken.
120
+ (config.showFigures === true &&
121
+ config.audio?.describeFigures === true &&
122
+ config.sources.audio === "generate" &&
123
+ config.sources.video === "generate") ||
114
124
  usesChapterCards(config) ||
115
125
  (usesAnimation(config) && videoEditOf(config).animate === "chapters"));
116
126
  }