@gentbajko/slopify 2.5.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (542) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +24 -41
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +31 -0
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +85 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/gemini.js +206 -0
  44. package/dist/adapters/tts/inworld-async.js +9 -5
  45. package/dist/adapters/tts/inworld.js +17 -3
  46. package/dist/adapters/tts/openai.js +3 -0
  47. package/dist/adapters/tts/pcm-mp3.js +62 -0
  48. package/dist/adapters/tts/system.js +147 -0
  49. package/dist/adapters/tts/voice-languages.js +80 -0
  50. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  55. package/dist/assets/fonts/noto/OFL.txt +93 -0
  56. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  57. package/dist/assets/models.yaml +38 -0
  58. package/dist/assets/sample/sample-audiobook.tar +0 -0
  59. package/dist/assets/sample/sample-podcast.tar +0 -0
  60. package/dist/assets/sample/sample-project.tar +0 -0
  61. package/dist/assets/style-preview/narration.mp3 +0 -0
  62. package/dist/assets/style-preview/tall-1.jpg +0 -0
  63. package/dist/assets/style-preview/tall-2.jpg +0 -0
  64. package/dist/assets/style-preview/tall-3.jpg +0 -0
  65. package/dist/assets/style-preview/wide-1.jpg +0 -0
  66. package/dist/assets/style-preview/wide-2.jpg +0 -0
  67. package/dist/assets/style-preview/wide-3.jpg +0 -0
  68. package/dist/catalog/merge.js +132 -0
  69. package/dist/catalog/registry.js +17 -14
  70. package/dist/catalog/schema.js +2 -0
  71. package/dist/catalog/store.js +147 -8
  72. package/dist/catalog/validate.js +2 -2
  73. package/dist/compose.yaml +71 -0
  74. package/dist/edge/autostart/docker-record.js +32 -0
  75. package/dist/edge/autostart/docker.js +118 -0
  76. package/dist/edge/autostart/index.js +78 -0
  77. package/dist/edge/autostart/launcher.js +176 -0
  78. package/dist/edge/autostart/model.js +3 -0
  79. package/dist/edge/autostart/native.js +210 -0
  80. package/dist/edge/autostart/prompt.js +72 -0
  81. package/dist/edge/autostart/service.js +90 -0
  82. package/dist/edge/cli-args.js +71 -0
  83. package/dist/edge/cli.js +64 -63
  84. package/dist/edge/docker-install/activation.js +56 -0
  85. package/dist/edge/docker-install/apply.js +458 -0
  86. package/dist/edge/docker-install/engine.js +362 -0
  87. package/dist/edge/docker-install/run.js +160 -0
  88. package/dist/edge/docker-install/state.js +146 -0
  89. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  90. package/dist/edge/docker.js +14 -22
  91. package/dist/edge/events/hub.js +3 -1
  92. package/dist/edge/host-cli.js +1 -1
  93. package/dist/edge/http/actions.js +38 -11
  94. package/dist/edge/http/app.js +60 -2
  95. package/dist/edge/http/auditions.js +70 -0
  96. package/dist/edge/http/autostart.js +37 -0
  97. package/dist/edge/http/byte-range.js +42 -0
  98. package/dist/edge/http/channel-memory.js +92 -0
  99. package/dist/edge/http/channels.js +128 -0
  100. package/dist/edge/http/diagnostics.js +1 -0
  101. package/dist/edge/http/draft-files.js +2 -4
  102. package/dist/edge/http/entries.js +22 -3
  103. package/dist/edge/http/files.js +11 -7
  104. package/dist/edge/http/folder-location.js +1 -1
  105. package/dist/edge/http/home.js +39 -0
  106. package/dist/edge/http/host-cli.js +29 -5
  107. package/dist/edge/http/narration-peaks.js +76 -0
  108. package/dist/edge/http/onboarding.js +212 -0
  109. package/dist/edge/http/open-folder.js +1 -1
  110. package/dist/edge/http/patch-notes.js +45 -0
  111. package/dist/edge/http/planning.js +4 -2
  112. package/dist/edge/http/project-create.js +87 -0
  113. package/dist/edge/http/project-templates.js +19 -2
  114. package/dist/edge/http/projects.js +37 -100
  115. package/dist/edge/http/prompts.js +40 -3
  116. package/dist/edge/http/pronunciations.js +23 -4
  117. package/dist/edge/http/providers.js +119 -3
  118. package/dist/edge/http/reviews.js +131 -0
  119. package/dist/edge/http/revision-files.js +3 -5
  120. package/dist/edge/http/revisions.js +1 -1
  121. package/dist/edge/http/run-cost.js +25 -0
  122. package/dist/edge/http/schedules.js +115 -6
  123. package/dist/edge/http/settings.js +84 -6
  124. package/dist/edge/http/storage-files.js +50 -0
  125. package/dist/edge/http/storage.js +43 -2
  126. package/dist/edge/http/studio.js +287 -0
  127. package/dist/edge/http/style-preview.js +61 -0
  128. package/dist/edge/http/subtitles.js +1 -1
  129. package/dist/edge/http/trash.js +69 -0
  130. package/dist/edge/http/tutorials.js +59 -0
  131. package/dist/edge/http/update.js +24 -3
  132. package/dist/edge/http/whats-new.js +15 -0
  133. package/dist/edge/http/youtube-edits.js +92 -0
  134. package/dist/edge/native-update.js +62 -0
  135. package/dist/extension/slopify-studio-chrome.zip +0 -0
  136. package/dist/extension/slopify-studio-firefox.zip +0 -0
  137. package/dist/host-cli/gemini-login.js +83 -0
  138. package/dist/host-cli/open-folder.js +10 -10
  139. package/dist/host-cli/runtime.js +9 -1
  140. package/dist/host-cli/status.js +4 -2
  141. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  142. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  143. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  144. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  145. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  146. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  147. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  148. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  149. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  150. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  151. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  152. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  153. package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
  154. package/dist/kernel/paths.js +27 -6
  155. package/dist/kernel/pipeline.js +4 -1
  156. package/dist/kernel/ports/host-cli.js +122 -14
  157. package/dist/kernel/ports/key-probe.js +1 -0
  158. package/dist/kernel/ports/languages.js +157 -0
  159. package/dist/kernel/ports/model.js +10 -5
  160. package/dist/kernel/ports/narration-aliases.js +60 -0
  161. package/dist/kernel/ports/plan-limits.js +36 -0
  162. package/dist/kernel/ports/system-speech.js +203 -0
  163. package/dist/kernel/runner/attempt.js +17 -8
  164. package/dist/kernel/runner/audition.js +36 -0
  165. package/dist/kernel/runner/graph.js +40 -5
  166. package/dist/kernel/runner/index.js +39 -9
  167. package/dist/kernel/runner/meter.js +1 -0
  168. package/dist/kernel/runner/providers.js +142 -7
  169. package/dist/kernel/runner/retry-policy.js +43 -0
  170. package/dist/kernel/runner/standalone.js +134 -0
  171. package/dist/kernel/runner/work-authority.js +28 -2
  172. package/dist/main.js +277 -16
  173. package/dist/model-catalog.js +2 -0
  174. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  175. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  176. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  177. package/dist/patch-notes/3.0.0.md +70 -0
  178. package/dist/patch-notes/3.0.1.md +105 -0
  179. package/dist/patch-notes/index.json +32 -0
  180. package/dist/slices/admission/model.js +27 -0
  181. package/dist/slices/admission/repo.js +24 -4
  182. package/dist/slices/admission/rules.js +80 -2
  183. package/dist/slices/admission/schema.js +58 -1
  184. package/dist/slices/admission/short-mode.js +47 -0
  185. package/dist/slices/admission/start.js +42 -6
  186. package/dist/slices/article/continuation.js +2 -2
  187. package/dist/slices/article/provided-entries.js +2 -2
  188. package/dist/slices/article/run.js +6 -2
  189. package/dist/slices/article/segments.js +3 -3
  190. package/dist/slices/backups/folder.js +14 -6
  191. package/dist/slices/batch/index.js +7 -2
  192. package/dist/slices/cancel/index.js +1 -1
  193. package/dist/slices/channels/cast-images.js +110 -0
  194. package/dist/slices/channels/cast-match.js +32 -0
  195. package/dist/slices/channels/images.js +19 -0
  196. package/dist/slices/channels/model.js +5 -0
  197. package/dist/slices/channels/rebrand.js +100 -0
  198. package/dist/slices/channels/repo.js +221 -0
  199. package/dist/slices/channels/runs.js +129 -0
  200. package/dist/slices/channels/schema.js +152 -0
  201. package/dist/slices/channels/service.js +180 -0
  202. package/dist/slices/channels/studio-csv.js +110 -0
  203. package/dist/slices/channels/videos.js +120 -0
  204. package/dist/slices/checkpoints/recovery.js +10 -0
  205. package/dist/slices/control/index.js +5 -2
  206. package/dist/slices/control/providers.js +4 -1
  207. package/dist/slices/document/fonts.js +1 -1
  208. package/dist/slices/document/render.js +1 -1
  209. package/dist/slices/episodes/related.js +90 -0
  210. package/dist/slices/episodes/repo.js +60 -0
  211. package/dist/slices/episodes/service.js +58 -0
  212. package/dist/slices/episodes/summarize.js +170 -0
  213. package/dist/slices/estimate/index.js +109 -28
  214. package/dist/slices/estimate/requests.js +68 -2
  215. package/dist/slices/eta/history.js +56 -0
  216. package/dist/slices/eta/model.js +63 -0
  217. package/dist/slices/eta/view.js +25 -0
  218. package/dist/slices/fixes/rules.js +73 -0
  219. package/dist/slices/fonts/coverage.js +82 -0
  220. package/dist/slices/fonts/sfnt.js +74 -0
  221. package/dist/slices/images/run.js +8 -5
  222. package/dist/slices/images/scale.js +84 -0
  223. package/dist/slices/library/diff.js +122 -0
  224. package/dist/slices/library/history.js +100 -0
  225. package/dist/slices/library/model.js +3 -0
  226. package/dist/slices/library/photorealistic.js +49 -0
  227. package/dist/slices/library/repo.js +19 -14
  228. package/dist/slices/library/save.js +62 -12
  229. package/dist/slices/library/slots.js +7 -1
  230. package/dist/slices/library/used-by.js +153 -0
  231. package/dist/slices/loudness/level-pieces.js +41 -0
  232. package/dist/slices/loudness/loudnorm.js +164 -0
  233. package/dist/slices/loudness/model.js +153 -0
  234. package/dist/slices/model-upkeep/model.js +10 -0
  235. package/dist/slices/model-upkeep/switch.js +217 -0
  236. package/dist/slices/model-upkeep/usage.js +185 -0
  237. package/dist/slices/narration/aliases-library.js +31 -0
  238. package/dist/slices/narration/aliases-schema.js +53 -0
  239. package/dist/slices/narration/aliases.js +49 -0
  240. package/dist/slices/narration/blocks.js +352 -0
  241. package/dist/slices/narration/chunk.js +19 -7
  242. package/dist/slices/narration/concat.js +2 -2
  243. package/dist/slices/narration/describe.js +45 -0
  244. package/dist/slices/narration/pauses-model.js +63 -0
  245. package/dist/slices/narration/pauses.js +269 -0
  246. package/dist/slices/narration/peaks-model.js +1 -0
  247. package/dist/slices/narration/peaks.js +94 -0
  248. package/dist/slices/narration/preparation.js +21 -3
  249. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  250. package/dist/slices/narration/pronunciation.js +72 -25
  251. package/dist/slices/narration/run.js +6 -6
  252. package/dist/slices/narration/shared-glossary.js +1 -1
  253. package/dist/slices/narration/steering.js +15 -4
  254. package/dist/slices/notifications/notifier.js +47 -3
  255. package/dist/slices/notifications/rules.js +48 -3
  256. package/dist/slices/onboarding/copy.js +143 -0
  257. package/dist/slices/onboarding/first-run.js +72 -0
  258. package/dist/slices/onboarding/full-video.js +81 -0
  259. package/dist/slices/onboarding/install.js +119 -0
  260. package/dist/slices/onboarding/model.js +85 -0
  261. package/dist/slices/onboarding/packs.js +491 -0
  262. package/dist/slices/onboarding/quick-short.js +144 -0
  263. package/dist/slices/onboarding/sample.js +132 -0
  264. package/dist/slices/onboarding/state.js +68 -0
  265. package/dist/slices/onboarding/template.js +73 -0
  266. package/dist/slices/patch-notes/library.js +73 -0
  267. package/dist/slices/patch-notes/seen.js +77 -0
  268. package/dist/slices/play-drafts/convert.js +127 -8
  269. package/dist/slices/play-drafts/readiness.js +4 -1
  270. package/dist/slices/play-drafts/repo.js +2 -0
  271. package/dist/slices/play-drafts/review-inputs.js +20 -3
  272. package/dist/slices/play-drafts/schema.js +89 -0
  273. package/dist/slices/play-drafts/service.js +4 -1
  274. package/dist/slices/play-drafts/start.js +43 -18
  275. package/dist/slices/project-templates/from-project.js +59 -6
  276. package/dist/slices/project-templates/next-chapter.js +94 -0
  277. package/dist/slices/project-templates/one-off.js +26 -0
  278. package/dist/slices/project-templates/repo.js +2 -1
  279. package/dist/slices/project-templates/schema.js +7 -1
  280. package/dist/slices/project-templates/service.js +15 -4
  281. package/dist/slices/project-templates/setup.js +18 -2
  282. package/dist/slices/rebuild/admission-repo.js +1 -1
  283. package/dist/slices/rebuild/model.js +6 -0
  284. package/dist/slices/rebuild/preview-details.js +46 -3
  285. package/dist/slices/rebuild/preview-plan.js +13 -6
  286. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  287. package/dist/slices/rebuild/recipe-audio.js +66 -12
  288. package/dist/slices/rebuild/recipe-build.js +8 -4
  289. package/dist/slices/rebuild/recipe-cast.js +18 -0
  290. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  291. package/dist/slices/rebuild/recipe-describe.js +93 -0
  292. package/dist/slices/rebuild/recipe-edit.js +13 -1
  293. package/dist/slices/rebuild/recipe-exports.js +67 -8
  294. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  295. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  296. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  297. package/dist/slices/rebuild/recipe-model.js +25 -6
  298. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  299. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  300. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  301. package/dist/slices/rebuild/recipe-reference.js +15 -1
  302. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  303. package/dist/slices/rebuild/recipe-save.js +4 -1
  304. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  305. package/dist/slices/rebuild/recipe-text.js +114 -19
  306. package/dist/slices/rebuild/recipe-validation.js +14 -2
  307. package/dist/slices/rebuild/recipe-visual.js +96 -33
  308. package/dist/slices/rebuild/recipe-voices.js +286 -0
  309. package/dist/slices/rebuild/recipe-work.js +77 -6
  310. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  311. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  312. package/dist/slices/rebuild/recovery-model.js +3 -0
  313. package/dist/slices/rebuild/recovery-selection.js +28 -2
  314. package/dist/slices/rebuild/recovery.js +35 -17
  315. package/dist/slices/rebuild/repo.js +3 -3
  316. package/dist/slices/rebuild/retry.js +18 -0
  317. package/dist/slices/rebuild/review-redo.js +125 -0
  318. package/dist/slices/rebuild/runtime-actions.js +5 -0
  319. package/dist/slices/rebuild/runtime-admission.js +4 -1
  320. package/dist/slices/rebuild/runtime-animate.js +4 -4
  321. package/dist/slices/rebuild/runtime-article.js +5 -5
  322. package/dist/slices/rebuild/runtime-document.js +4 -4
  323. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  324. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  325. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  326. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  327. package/dist/slices/rebuild/runtime-export.js +84 -19
  328. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  329. package/dist/slices/rebuild/runtime-image.js +21 -2
  330. package/dist/slices/rebuild/runtime-local.js +148 -24
  331. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  332. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  333. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  334. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  335. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  336. package/dist/slices/rebuild/runtime-provider.js +126 -18
  337. package/dist/slices/rebuild/runtime-publication.js +21 -5
  338. package/dist/slices/rebuild/runtime-review.js +267 -0
  339. package/dist/slices/rebuild/runtime-run.js +17 -11
  340. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  341. package/dist/slices/rebuild/runtime-store.js +62 -11
  342. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  343. package/dist/slices/rebuild/runtime-voices.js +103 -0
  344. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  345. package/dist/slices/rebuild/service-readiness.js +3 -3
  346. package/dist/slices/rebuild/soften.js +49 -0
  347. package/dist/slices/rebuild/transition-repo.js +9 -2
  348. package/dist/slices/research/run.js +4 -4
  349. package/dist/slices/research/synthesis.js +2 -2
  350. package/dist/slices/reviews/model.js +105 -0
  351. package/dist/slices/reviews/outcome.js +22 -0
  352. package/dist/slices/reviews/repo.js +118 -0
  353. package/dist/slices/reviews/rules.js +63 -0
  354. package/dist/slices/reviews/verdict.js +71 -0
  355. package/dist/slices/reviews/view.js +30 -0
  356. package/dist/slices/revisions/adopt-content.js +5 -2
  357. package/dist/slices/revisions/adopt.js +3 -1
  358. package/dist/slices/revisions/downloads.js +7 -1
  359. package/dist/slices/revisions/image-plan.js +183 -0
  360. package/dist/slices/revisions/mutation-assets.js +10 -0
  361. package/dist/slices/revisions/mutation-cues.js +71 -0
  362. package/dist/slices/revisions/mutation-request.js +2 -1
  363. package/dist/slices/revisions/mutations.js +97 -3
  364. package/dist/slices/revisions/schema.js +11 -1
  365. package/dist/slices/run-cost/limits.js +128 -0
  366. package/dist/slices/run-cost/meter.js +47 -0
  367. package/dist/slices/run-cost/panel.js +256 -0
  368. package/dist/slices/run-cost/pricing.js +95 -0
  369. package/dist/slices/run-cost/week.js +108 -0
  370. package/dist/slices/schedules/agenda.js +222 -0
  371. package/dist/slices/schedules/repo.js +58 -18
  372. package/dist/slices/schedules/scheduler.js +68 -17
  373. package/dist/slices/schedules/schema.js +179 -1
  374. package/dist/slices/schedules/service.js +43 -4
  375. package/dist/slices/schedules/similar.js +128 -0
  376. package/dist/slices/schedules/topic-list.js +163 -0
  377. package/dist/slices/schedules/topics.js +523 -0
  378. package/dist/slices/settings/first-run.js +106 -0
  379. package/dist/slices/settings/health.js +247 -0
  380. package/dist/slices/settings/key-guides.js +138 -0
  381. package/dist/slices/settings/key-test.js +93 -0
  382. package/dist/slices/settings/keys.js +10 -5
  383. package/dist/slices/settings/model-reach.js +52 -0
  384. package/dist/slices/settings/model.js +55 -1
  385. package/dist/slices/settings/models.js +5 -1
  386. package/dist/slices/settings/playback.js +15 -0
  387. package/dist/slices/settings/readiness.js +14 -2
  388. package/dist/slices/settings/repo.js +17 -3
  389. package/dist/slices/settings/tutorial-schema.js +5 -0
  390. package/dist/slices/settings/voices.js +64 -4
  391. package/dist/slices/settings/whats-new.js +64 -0
  392. package/dist/slices/shorts/captions.js +9 -0
  393. package/dist/slices/shorts/pick.js +3 -3
  394. package/dist/slices/shorts/prompts.js +1 -1
  395. package/dist/slices/shorts/render.js +63 -13
  396. package/dist/slices/storage/asset-name.js +3 -0
  397. package/dist/slices/storage/backup-export.js +48 -9
  398. package/dist/slices/storage/backup-format.js +38 -1
  399. package/dist/slices/storage/backup-import.js +191 -19
  400. package/dist/slices/storage/delete-project.js +7 -4
  401. package/dist/slices/storage/documents.js +87 -0
  402. package/dist/slices/storage/downloads.js +33 -1
  403. package/dist/slices/storage/files-location.js +531 -0
  404. package/dist/slices/storage/layout.js +14 -4
  405. package/dist/slices/storage/model.js +11 -0
  406. package/dist/slices/storage/portable.js +94 -12
  407. package/dist/slices/storage/reconcile.js +14 -1
  408. package/dist/slices/storage/repo.js +4 -1
  409. package/dist/slices/storage/schema.js +11 -0
  410. package/dist/slices/storage/trim.js +146 -0
  411. package/dist/slices/studio/disclosure.js +74 -0
  412. package/dist/slices/studio/model.js +30 -0
  413. package/dist/slices/studio/pack.js +211 -0
  414. package/dist/slices/studio/queue.js +69 -0
  415. package/dist/slices/studio/settings.js +170 -0
  416. package/dist/slices/style-preview/images.js +61 -0
  417. package/dist/slices/style-preview/narration.js +33 -0
  418. package/dist/slices/style-preview/render.js +148 -0
  419. package/dist/slices/style-preview/schema.js +59 -0
  420. package/dist/slices/style-preview/service.js +92 -0
  421. package/dist/slices/style-preview/settings.js +139 -0
  422. package/dist/slices/subtitles/captions.js +42 -9
  423. package/dist/slices/subtitles/model.js +22 -0
  424. package/dist/slices/subtitles/prepare.js +27 -16
  425. package/dist/slices/subtitles/transcript.js +1 -1
  426. package/dist/slices/thumbnail/run.js +7 -7
  427. package/dist/slices/trash/model.js +25 -0
  428. package/dist/slices/trash/service.js +273 -0
  429. package/dist/slices/tutorials/anchors.js +57 -0
  430. package/dist/slices/tutorials/library.js +185 -0
  431. package/dist/slices/uploads/repo.js +20 -0
  432. package/dist/slices/video/ambient-bed-schema.js +15 -0
  433. package/dist/slices/video/ambient-bed.js +95 -0
  434. package/dist/slices/video/ambient-mix.js +72 -0
  435. package/dist/slices/video/audio-inputs.js +2 -2
  436. package/dist/slices/video/cards.js +5 -2
  437. package/dist/slices/video/edit-list.js +23 -2
  438. package/dist/slices/video/edit-settings.js +13 -3
  439. package/dist/slices/video/ffmpeg.js +63 -9
  440. package/dist/slices/video/figure-card.js +492 -0
  441. package/dist/slices/video/figure-spans.js +51 -0
  442. package/dist/slices/video/plan.js +125 -13
  443. package/dist/slices/video/run.js +14 -12
  444. package/dist/slices/video/slideshow.js +39 -4
  445. package/dist/slices/voices/attribution.js +69 -0
  446. package/dist/slices/voices/audio-files.js +87 -0
  447. package/dist/slices/voices/audition.js +25 -0
  448. package/dist/slices/voices/cast.js +74 -0
  449. package/dist/slices/voices/grouping.js +59 -0
  450. package/dist/slices/voices/join.js +35 -0
  451. package/dist/slices/voices/languages.js +25 -0
  452. package/dist/slices/voices/model.js +218 -0
  453. package/dist/slices/voices/palette.js +26 -0
  454. package/dist/slices/voices/panel.js +91 -0
  455. package/dist/slices/voices/portraits.js +55 -0
  456. package/dist/slices/voices/script.js +136 -0
  457. package/dist/slices/voices/timing.js +58 -0
  458. package/dist/slices/youtube/answer.js +1 -1
  459. package/dist/slices/youtube/chapters.js +126 -0
  460. package/dist/slices/youtube/edits-repo.js +84 -0
  461. package/dist/slices/youtube/edits.js +89 -0
  462. package/dist/slices/youtube/placeholders.js +98 -0
  463. package/dist/slices/youtube/transcript.js +11 -4
  464. package/dist/tutorials/AI-CLIs.md +146 -0
  465. package/dist/tutorials/Backups.md +121 -0
  466. package/dist/tutorials/Calendar.md +103 -0
  467. package/dist/tutorials/Cast-Library.md +112 -0
  468. package/dist/tutorials/Channels.md +148 -0
  469. package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
  470. package/dist/tutorials/Docker.md +171 -0
  471. package/dist/tutorials/Document-Themes.md +207 -0
  472. package/dist/tutorials/Editing-a-Project.md +204 -0
  473. package/dist/tutorials/FAQ.md +134 -0
  474. package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
  475. package/dist/tutorials/Home-and-Projects.md +174 -0
  476. package/dist/tutorials/Home.md +119 -0
  477. package/dist/tutorials/Install.md +154 -0
  478. package/dist/tutorials/Intros-and-Outros.md +86 -0
  479. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
  480. package/dist/tutorials/Library-Overview.md +75 -0
  481. package/dist/tutorials/Models.md +133 -0
  482. package/dist/tutorials/Multiple-Voices.md +161 -0
  483. package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
  484. package/dist/tutorials/Notifications.md +86 -0
  485. package/dist/tutorials/Other-Languages.md +110 -0
  486. package/dist/tutorials/PDF-Documents.md +100 -0
  487. package/dist/tutorials/Play-Images.md +135 -0
  488. package/dist/tutorials/Play-Narration.md +167 -0
  489. package/dist/tutorials/Play-Outputs.md +109 -0
  490. package/dist/tutorials/Play-Overview.md +216 -0
  491. package/dist/tutorials/Play-Title-and-Article.md +135 -0
  492. package/dist/tutorials/Play-Video-and-Style.md +143 -0
  493. package/dist/tutorials/Project-Page.md +228 -0
  494. package/dist/tutorials/Prompts.md +136 -0
  495. package/dist/tutorials/Providers-and-Keys.md +290 -0
  496. package/dist/tutorials/Publishing-to-YouTube.md +135 -0
  497. package/dist/tutorials/Recovery-and-Retries.md +141 -0
  498. package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
  499. package/dist/tutorials/Schedules.md +154 -0
  500. package/dist/tutorials/Settings-Reference.md +258 -0
  501. package/dist/tutorials/Shorts.md +116 -0
  502. package/dist/tutorials/Start-at-Login.md +126 -0
  503. package/dist/tutorials/Studio-Extension.md +146 -0
  504. package/dist/tutorials/Templates.md +146 -0
  505. package/dist/tutorials/Trash.md +87 -0
  506. package/dist/tutorials/Troubleshooting.md +149 -0
  507. package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
  508. package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
  509. package/dist/tutorials/Video-Editing.md +135 -0
  510. package/dist/tutorials/Where-Your-Files-Live.md +149 -0
  511. package/dist/tutorials/YouTube-Description.md +132 -0
  512. package/dist/tutorials/Your-First-Short.md +125 -0
  513. package/dist/tutorials/_Footer.md +1 -0
  514. package/dist/tutorials/_Sidebar.md +69 -0
  515. package/dist/updater/service.js +66 -21
  516. package/dist/web/assets/index-DBLSfqA-.css +1 -0
  517. package/dist/web/assets/index-DFdIhmm0.js +307 -0
  518. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  519. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  520. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  521. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  522. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  523. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  524. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  525. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  526. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  527. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
  528. package/dist/web/index.html +2 -2
  529. package/package.json +5 -5
  530. package/dist/edge/docker-launch.js +0 -81
  531. package/dist/edge/docker-projects/activation.js +0 -35
  532. package/dist/edge/docker-projects/claims.js +0 -85
  533. package/dist/edge/docker-projects/committed.js +0 -40
  534. package/dist/edge/docker-projects/engine.js +0 -461
  535. package/dist/edge/docker-projects/install.js +0 -254
  536. package/dist/edge/docker-projects/prune.js +0 -66
  537. package/dist/edge/docker-projects/recover.js +0 -100
  538. package/dist/edge/docker-projects/state.js +0 -225
  539. package/dist/web/assets/index-CktpxlJd.js +0 -171
  540. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  541. package/scripts/docker-run.sh +0 -3
  542. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,185 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { pageTitleOf, tutorialHeadings, tutorialPageIdPattern } from "./anchors.js";
6
+ // dist/tutorials when running the build; the repository's docs/wiki when running from source
7
+ // (tests, `tsx`).
8
+ export function bundledTutorialsDir() {
9
+ const candidates = ["../../tutorials/", "../../../../../docs/wiki/"].map((path) => fileURLToPath(new URL(path, import.meta.url)));
10
+ return candidates.find((dir) => existsSync(join(dir, "Home.md"))) ?? candidates[0];
11
+ }
12
+ export class TutorialsMissing extends Error {
13
+ }
14
+ // `[Title](Page-Name)` links of the sidebar, under its `**Group**` lines. The first link
15
+ // before any group is the home page.
16
+ export function parseSidebar(markdown, titleOf = () => undefined) {
17
+ const groups = [];
18
+ let home;
19
+ for (const line of markdown.replace(/\r\n?/g, "\n").split("\n")) {
20
+ const group = /^\s*\*\*([^*[\]]+)\*\*\s*$/.exec(line);
21
+ if (group !== null) {
22
+ groups.push({ title: (group[1] ?? "").trim(), pages: [] });
23
+ continue;
24
+ }
25
+ for (const link of line.matchAll(/\[([^\]]+)\]\(([^)#\s]+)\)/g)) {
26
+ const id = link[2] ?? "";
27
+ if (!tutorialPageIdPattern.test(id))
28
+ continue;
29
+ const page = { id, title: (link[1] ?? titleOf(id) ?? pageTitleOf(id)).trim() };
30
+ const last = groups[groups.length - 1];
31
+ if (last === undefined)
32
+ home ??= page;
33
+ else
34
+ last.pages.push(page);
35
+ }
36
+ }
37
+ return { home, groups: groups.filter((group) => group.pages.length > 0) };
38
+ }
39
+ // Reads every page once; they never change while the app runs.
40
+ export async function loadTutorials(dir) {
41
+ let names;
42
+ try {
43
+ names = await readdir(dir);
44
+ }
45
+ catch (error) {
46
+ if (error.code === "ENOENT")
47
+ throw new TutorialsMissing("This copy of Slopify has no tutorials: its docs/wiki folder is missing. Reinstall Slopify (or run npm run build) and reload the page.");
48
+ throw error;
49
+ }
50
+ const pages = new Map();
51
+ for (const name of names.sort()) {
52
+ const id = name.replace(/\.md$/, "");
53
+ if (!name.endsWith(".md") || !tutorialPageIdPattern.test(id))
54
+ continue;
55
+ pages.set(id, await readFile(join(dir, name), "utf8"));
56
+ }
57
+ if (!pages.has("Home"))
58
+ throw new TutorialsMissing("This copy of Slopify has damaged tutorials: docs/wiki/Home.md is missing. Reinstall Slopify (or run npm run build) and reload the page.");
59
+ const extra = async (name) => readFile(join(dir, name), "utf8").catch((error) => {
60
+ if (error.code === "ENOENT")
61
+ return "";
62
+ throw error;
63
+ });
64
+ const sidebar = parseSidebar(await extra("_Sidebar.md"));
65
+ const groups = sidebar.groups
66
+ .map((group) => ({ ...group, pages: group.pages.filter((page) => pages.has(page.id)) }))
67
+ .filter((group) => group.pages.length > 0);
68
+ const listed = new Map();
69
+ const home = sidebar.home ?? { id: "Home", title: "Home" };
70
+ listed.set(home.id, home);
71
+ for (const group of groups)
72
+ for (const page of group.pages)
73
+ listed.set(page.id, page);
74
+ for (const id of pages.keys())
75
+ if (!listed.has(id))
76
+ listed.set(id, { id, title: pageTitleOf(id) });
77
+ return {
78
+ index: {
79
+ home,
80
+ groups,
81
+ pages: [...listed.values()],
82
+ footer: (await extra("_Footer.md")).trim(),
83
+ },
84
+ pages,
85
+ };
86
+ }
87
+ // Plain words of a Markdown line: links become their text, marks and tables' pipes go.
88
+ function plain(markdown) {
89
+ return markdown
90
+ .replace(/^\s*\|?\s*:?-{3,}[-|: ]*$/gm, "")
91
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
92
+ .replace(/[`*_>|]/g, " ")
93
+ .replace(/^\s*(?:[-+]|\d+\.)\s+/gm, "")
94
+ .replace(/\s+/g, " ")
95
+ .trim();
96
+ }
97
+ function sectionsOf(markdown) {
98
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
99
+ const headings = tutorialHeadings(markdown);
100
+ const sections = [];
101
+ const first = headings[0]?.line ?? lines.length;
102
+ sections.push({ anchor: "", heading: undefined, text: plain(lines.slice(0, first).join("\n")) });
103
+ for (const [at, heading] of headings.entries()) {
104
+ const end = headings[at + 1]?.line ?? lines.length;
105
+ sections.push({
106
+ anchor: heading.anchor,
107
+ heading: heading.text,
108
+ text: plain(lines.slice(heading.line + 1, end).join("\n")),
109
+ });
110
+ }
111
+ return sections.filter((section) => section.heading !== undefined || section.text !== "");
112
+ }
113
+ function snippetOf(text, term) {
114
+ const at = term === "" ? -1 : text.toLowerCase().indexOf(term);
115
+ if (at < 0)
116
+ return text.length > 180 ? `${text.slice(0, 177).trimEnd()}…` : text;
117
+ // Whole words at both ends.
118
+ const from = Math.max(0, at - 60);
119
+ const start = from === 0 ? 0 : text.indexOf(" ", from) + 1 || from;
120
+ const to = Math.min(text.length, at + term.length + 120);
121
+ const end = to === text.length ? to : text.lastIndexOf(" ", to) > at ? text.lastIndexOf(" ", to) : to;
122
+ return `${start > 0 ? "…" : ""}${text.slice(start, end).trim()}${end < text.length ? "…" : ""}`;
123
+ }
124
+ // The sections that hold every word typed, best first: a word in the page's title or the
125
+ // section's heading counts more than one in its text. The first heading (the page's own
126
+ // title) stands for the page.
127
+ export function searchTutorials(book, query, limit = 30) {
128
+ const terms = query
129
+ .toLowerCase()
130
+ .split(/\s+/)
131
+ .map((term) => term.trim())
132
+ .filter((term) => term.length > 0)
133
+ .slice(0, 8);
134
+ if (terms.length === 0)
135
+ return [];
136
+ const titles = new Map(book.index.pages.map((page) => [page.id, page.title]));
137
+ const scored = [];
138
+ let order = 0;
139
+ for (const summary of book.index.pages) {
140
+ const markdown = book.pages.get(summary.id);
141
+ if (markdown === undefined)
142
+ continue;
143
+ const pageTitle = titles.get(summary.id) ?? pageTitleOf(summary.id);
144
+ for (const section of sectionsOf(markdown)) {
145
+ order += 1;
146
+ const heading = (section.heading ?? "").toLowerCase();
147
+ const text = section.text.toLowerCase();
148
+ const title = pageTitle.toLowerCase();
149
+ let score = 0;
150
+ let all = true;
151
+ let own = false;
152
+ for (const term of terms) {
153
+ const inHeading = heading.includes(term);
154
+ const inTitle = title.includes(term);
155
+ const inText = text.includes(term);
156
+ if (!inHeading && !inText && !inTitle) {
157
+ all = false;
158
+ break;
159
+ }
160
+ own ||= inHeading || inText;
161
+ score += (inHeading ? 6 : 0) + (inTitle ? 3 : 0) + (inText ? 1 : 0);
162
+ }
163
+ // A word only in the page's title would list every section of the page.
164
+ if (!all || !own)
165
+ continue;
166
+ if (heading === query.trim().toLowerCase())
167
+ score += 10;
168
+ scored.push({
169
+ hit: {
170
+ page: summary.id,
171
+ pageTitle,
172
+ anchor: section.anchor,
173
+ heading: section.heading,
174
+ snippet: snippetOf(section.text, terms.find((term) => text.includes(term)) ?? ""),
175
+ },
176
+ score,
177
+ order,
178
+ });
179
+ }
180
+ }
181
+ return scored
182
+ .sort((a, b) => b.score - a.score || a.order - b.order)
183
+ .slice(0, limit)
184
+ .map(({ hit }) => hit);
185
+ }
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { isAbsolute, resolve as resolvePath } from "node:path";
3
+ import { bedChains, bedInputs } from "./ambient-mix.js";
3
4
  import { gradeFilter, lookChain, lookEncoding, placed } from "./look.js";
4
5
  import { decimal, zoomRange } from "./plan.js";
5
6
  import { segments, transitionHalves } from "./transitions.js";
@@ -43,7 +44,7 @@ export function slideshowClips(edit) {
43
44
  named.set(key, name);
44
45
  const shot = edit.shots[segment.kind === "shot" ? segment.shot : segment.to];
45
46
  if (shot === undefined)
46
- throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use Re-run section on Video; if it happens again, use Download diagnostics in Settings and report it.");
47
+ throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
47
48
  clips.push({ name, segment, shot, frames: segment.count });
48
49
  }
49
50
  return name;
@@ -92,7 +93,7 @@ cardsFile = "cards.ass") {
92
93
  const chains = [];
93
94
  const picture = (shot, from, label) => {
94
95
  if (shot === undefined)
95
- throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use Re-run section on Video; if it happens again, use Download diagnostics in Settings and report it.");
96
+ throw new Error("Slopify hit an internal error (the video's edit list lost a shot). Use More → Render the video again in the Video section; if it happens again, use Download diagnostics in Settings and report it.");
96
97
  const at = inputs.filter((one) => one === "-i").length;
97
98
  const built = pictureChain(edit, shot, from, segment.count, at, label, gradeFilter(edit.look));
98
99
  inputs.push(...built.inputs);
@@ -207,11 +208,13 @@ function seconds6(value) {
207
208
  export function concatList(order) {
208
209
  return `ffconcat version 1.0\n${order.map((name) => `file ${name}`).join("\n")}\n`;
209
210
  }
210
- export function joinArgs(edit, output, list, burnSubtitles = false) {
211
- const inputs = ["-f", "concat", "-i", list];
211
+ // The video's sound: the narration's segments and silences joined, with the ambient bed under
212
+ // them when there is one, as the inputs from `first` on and the chains ending in `[a]`.
213
+ function audioMix(edit, first) {
214
+ const inputs = [];
212
215
  const audioAt = [];
213
216
  for (const segment of edit.audio) {
214
- audioAt.push(1 + audioAt.length);
217
+ audioAt.push(first + audioAt.length);
215
218
  if (segment.path === null) {
216
219
  inputs.push("-f", "lavfi", "-t", seconds(segment.seconds), "-i", `anullsrc=r=${sampleRate}:cl=${channelLayout}`);
217
220
  continue;
@@ -219,15 +222,65 @@ export function joinArgs(edit, output, list, burnSubtitles = false) {
219
222
  inputs.push("-i", segment.path);
220
223
  }
221
224
  const chains = [];
222
- if (burnSubtitles)
223
- chains.push("[0:v]ass=filename=subtitles.ass:fontsdir=fonts[v]");
224
225
  audioAt.forEach((input, at) => {
225
226
  // The segments come from different files and the silence from lavfi, so they are
226
227
  // brought to one format before concat, which refuses to join mismatched streams.
227
228
  chains.push(`[${input}:a]aformat=sample_fmts=fltp:sample_rates=${sampleRate}:channel_layouts=${channelLayout}[a${at}]`);
228
229
  });
230
+ // The ambient bed lies under the joined narration, ducked by it (`ambient-mix.ts`); without
231
+ // one the arguments are the ones every video was joined with.
232
+ const bed = audioAt.length > 0 ? edit.bed : undefined;
229
233
  if (audioAt.length > 0) {
230
- chains.push(`${audioAt.map((_input, at) => `[a${at}]`).join("")}concat=n=${audioAt.length}:v=0:a=1[a]`);
234
+ chains.push(`${audioAt.map((_input, at) => `[a${at}]`).join("")}concat=n=${audioAt.length}:v=0:a=1${bed === undefined ? "[a]" : "[narration]"}`);
235
+ }
236
+ if (bed !== undefined) {
237
+ const total = edit.audio.reduce((sum, segment) => sum + segment.seconds, 0);
238
+ inputs.push(...bedInputs(bed, total, sampleRate));
239
+ chains.push(...bedChains(bed, first + audioAt.length, total, `aformat=sample_fmts=fltp:sample_rates=${sampleRate}:channel_layouts=${channelLayout}`, "[narration]", "[a]"));
240
+ }
241
+ return { inputs, chains };
242
+ }
243
+ // The video's sound alone, as a WAV: what Level the volume masters before the join plays it
244
+ // (`slideshow.ts`).
245
+ export function audioMixArgs(edit, output) {
246
+ const mix = audioMix(edit, 0);
247
+ return [
248
+ ...progressArgs,
249
+ ...mix.inputs,
250
+ "-filter_complex",
251
+ mix.chains.join(";"),
252
+ "-map",
253
+ "[a]",
254
+ "-c:a",
255
+ "pcm_s16le",
256
+ "-f",
257
+ "wav",
258
+ output,
259
+ ];
260
+ }
261
+ export function joinArgs(edit, output, list, burnSubtitles = false, portraits = []) {
262
+ const mix = audioMix(edit, 1);
263
+ const inputs = ["-f", "concat", "-i", list, ...mix.inputs];
264
+ const chains = [];
265
+ const overlaid = burnSubtitles && portraits.length > 0;
266
+ if (burnSubtitles && !overlaid)
267
+ chains.push("[0:v]ass=filename=subtitles.ass:fontsdir=fonts[v]");
268
+ chains.push(...mix.chains);
269
+ // The portraits go in after every other input and under the captions, so the panel's lit
270
+ // outline is drawn over them; a video without any is joined exactly as before. A still
271
+ // image is one frame, which overlay holds to the end (`eof_action=repeat`).
272
+ if (overlaid) {
273
+ let first = inputs.filter((value) => value === "-i").length;
274
+ let source = "[0:v]";
275
+ const video = [];
276
+ portraits.forEach((portrait, at) => {
277
+ inputs.push("-i", portrait.path);
278
+ const size = String(portrait.size);
279
+ video.push(`[${String(first)}:v]scale=${size}:${size}:force_original_aspect_ratio=increase,crop=${size}:${size},setsar=1[pic${String(at)}]`, `${source}[pic${String(at)}]overlay=x=${String(portrait.x)}:y=${String(portrait.y)}:eof_action=repeat[panel${String(at)}]`);
280
+ source = `[panel${String(at)}]`;
281
+ first += 1;
282
+ });
283
+ chains.unshift(...video, `${source}ass=filename=subtitles.ass:fontsdir=fonts[v]`);
231
284
  }
232
285
  return [
233
286
  ...progressArgs,
@@ -359,6 +412,7 @@ export function runFfmpeg(run) {
359
412
  child.stderr.setEncoding("utf8");
360
413
  child.stderr.on("data", (chunk) => {
361
414
  guarded("video.stderr.read", () => {
415
+ run.onStderr?.(chunk);
362
416
  for (const line of chunk.split("\n")) {
363
417
  if (line.trim() !== "") {
364
418
  errors.push(line.trim());
@@ -436,5 +490,5 @@ function failure(code, signal, errors) {
436
490
  const said = errors.length === 0
437
491
  ? `ffmpeg ${ended} and wrote nothing to its error stream`
438
492
  : `ffmpeg ${ended}: ${errors.join(" / ")}`;
439
- return `The audio/video export failed (${said}). Retry stage; if it fails again, make sure your disk has free space (see Settings → Backup & storage), then use Download diagnostics in Settings and report it.`;
493
+ return `The audio/video export failed (${said}). Try again; if it fails again, make sure your disk has free space (see Settings → Backup & storage), then use Download diagnostics in Settings and report it.`;
440
494
  }