@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,153 @@
1
+ // "Used by": the templates, schedules and projects that name a Library prompt or intro/outro.
2
+ // Everything refers to the Library by name (a template's form, a schedule's template, a
3
+ // project revision's config), so the match is the name, without regard to case, within the
4
+ // prompt's kind or the entry's category - the same rule the unique indexes and
5
+ // `snapshot.ts` use.
6
+ import { z } from "zod";
7
+ import { liveProject } from "../admission/repo.js";
8
+ // The fields of a Play form or a run config that name Library items. Both shapes carry the
9
+ // same names; a run config holds the intro and outro as `{ name }` and a form as the name.
10
+ const named = z.string().optional().catch(undefined);
11
+ const refsShape = z
12
+ .object({
13
+ articlePrompt: named,
14
+ narrationPrompt: named,
15
+ thumbnailPrompt: named,
16
+ descriptionPrompt: named,
17
+ imagePrompts: z
18
+ .array(z.object({ name: z.string() }).loose())
19
+ .optional()
20
+ .catch(undefined),
21
+ reference: z.object({ prompt: named }).loose().optional().catch(undefined),
22
+ shorts: z.object({ prompt: named, imagePrompt: named }).loose().optional().catch(undefined),
23
+ intro: z
24
+ .union([z.string(), z.object({ name: z.string() }).loose()])
25
+ .optional()
26
+ .catch(undefined),
27
+ outro: z
28
+ .union([z.string(), z.object({ name: z.string() }).loose()])
29
+ .optional()
30
+ .catch(undefined),
31
+ })
32
+ .loose();
33
+ // The names a form or config holds for one kind of prompt or category of entry.
34
+ export function namesIn(value, ref) {
35
+ const parsed = refsShape.safeParse(value);
36
+ if (!parsed.success)
37
+ return [];
38
+ const form = parsed.data;
39
+ const entryName = (one) => one === undefined ? undefined : typeof one === "string" ? one : one.name;
40
+ const names = ref.item === "entry"
41
+ ? [ref.category === "intro" ? entryName(form.intro) : entryName(form.outro)]
42
+ : ref.kind === "article"
43
+ ? [form.articlePrompt]
44
+ : ref.kind === "narration"
45
+ ? [form.narrationPrompt]
46
+ : ref.kind === "thumbnail"
47
+ ? [form.thumbnailPrompt]
48
+ : ref.kind === "description"
49
+ ? [form.descriptionPrompt]
50
+ : ref.kind === "shorts"
51
+ ? [form.shorts?.prompt]
52
+ : [
53
+ ...(form.imagePrompts ?? []).map((one) => one.name),
54
+ form.reference?.prompt,
55
+ form.shorts?.imagePrompt,
56
+ ];
57
+ return names.filter((name) => name !== undefined && name.trim() !== "");
58
+ }
59
+ export function uses(value, ref) {
60
+ const wanted = ref.name.trim().toLowerCase();
61
+ return namesIn(value, ref).some((name) => name.trim().toLowerCase() === wanted);
62
+ }
63
+ const documentShape = z.object({ form: z.unknown() }).loose();
64
+ function formOf(json) {
65
+ const parsed = documentShape.safeParse(JSON.parse(json));
66
+ return parsed.success ? parsed.data.form : undefined;
67
+ }
68
+ export function usedBy(db, ref) {
69
+ // A name that appears nowhere in the JSON cannot match, so SQL skips those rows before any
70
+ // is parsed. JSON escapes quotes and backslashes, so only a plain name is pre-filtered.
71
+ // SQLite's lower() folds ASCII only, so a name with anything else is not pre-filtered.
72
+ const plain = /^[\x20-\x7e]*$/u.test(ref.name) && !/["\\]/u.test(ref.name)
73
+ ? ref.name.trim().toLowerCase()
74
+ : "";
75
+ const templates = db
76
+ .prepare(`SELECT t.id,r.name,r.document_json FROM project_templates t
77
+ JOIN project_template_revisions r ON r.template_id=t.id AND r.version=t.head_version
78
+ WHERE t.deleted_at IS NULL AND instr(lower(r.document_json),?)>0 ORDER BY lower(r.name),t.id`)
79
+ .all(plain)
80
+ .flatMap((row) => {
81
+ const parsed = templateRow.parse(row);
82
+ return uses(formOf(parsed.document_json), ref) ? [{ id: parsed.id, name: parsed.name }] : [];
83
+ });
84
+ // A schedule runs the template version it was saved with, so that version is what it uses.
85
+ const schedules = db
86
+ .prepare(`SELECT s.id,s.name,s.status,r.document_json FROM schedules s
87
+ JOIN project_template_revisions r ON r.template_id=s.template_id AND r.version=s.template_version
88
+ WHERE s.deleted_at IS NULL AND s.status IN ('active','paused') AND instr(lower(r.document_json),?)>0
89
+ ORDER BY lower(s.name),s.id`)
90
+ .all(plain)
91
+ .flatMap((row) => {
92
+ const parsed = scheduleRow.parse(row);
93
+ return uses(formOf(parsed.document_json), ref)
94
+ ? [{ id: parsed.id, name: parsed.name, status: parsed.status }]
95
+ : [];
96
+ });
97
+ const revisionRows = db
98
+ .prepare(`SELECT r.project_id,p.title,r.id AS revision_id,r.config,h.revision_id AS head,
99
+ (SELECT count(*) FROM project_revisions x WHERE x.project_id=r.project_id) AS total
100
+ FROM project_revisions r JOIN projects p ON p.id=r.project_id
101
+ LEFT JOIN project_heads h ON h.project_id=r.project_id
102
+ WHERE ${liveProject("p")} AND instr(lower(r.config),?)>0
103
+ ORDER BY p.created_at DESC,p.id,r.created_at`)
104
+ .all(plain)
105
+ .map((row) => revisionRow.parse(row));
106
+ const projects = new Map();
107
+ for (const row of revisionRows) {
108
+ if (!uses(JSON.parse(row.config), ref))
109
+ continue;
110
+ const seen = projects.get(row.project_id);
111
+ projects.set(row.project_id, {
112
+ id: row.project_id,
113
+ title: row.title,
114
+ revisions: (seen?.revisions ?? 0) + 1,
115
+ totalRevisions: row.total,
116
+ current: (seen?.current ?? false) || row.head === row.revision_id,
117
+ });
118
+ }
119
+ // A project from before revisions existed has none yet; its own config is what it ran with.
120
+ for (const row of db
121
+ .prepare(`SELECT p.id,p.title,p.config FROM projects p
122
+ WHERE ${liveProject("p")}
123
+ AND NOT EXISTS (SELECT 1 FROM project_revisions r WHERE r.project_id=p.id)
124
+ AND instr(lower(p.config),?)>0 ORDER BY p.created_at DESC,p.id`)
125
+ .all(plain)
126
+ .map((row) => legacyRow.parse(row))) {
127
+ if (uses(JSON.parse(row.config), ref))
128
+ projects.set(row.id, {
129
+ id: row.id,
130
+ title: row.title,
131
+ revisions: 0,
132
+ totalRevisions: 0,
133
+ current: true,
134
+ });
135
+ }
136
+ return { templates, schedules, projects: [...projects.values()] };
137
+ }
138
+ const templateRow = z.object({ id: z.string(), name: z.string(), document_json: z.string() });
139
+ const scheduleRow = z.object({
140
+ id: z.string(),
141
+ name: z.string(),
142
+ status: z.string(),
143
+ document_json: z.string(),
144
+ });
145
+ const revisionRow = z.object({
146
+ project_id: z.string(),
147
+ title: z.string(),
148
+ revision_id: z.string(),
149
+ config: z.string(),
150
+ head: z.string().nullable(),
151
+ total: z.number(),
152
+ });
153
+ const legacyRow = z.object({ id: z.string(), title: z.string(), config: z.string() });
@@ -0,0 +1,41 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { levelFile, measurable } from "./loudnorm.js";
4
+ import { pieceLufs, pieceTruePeak, spreadOf } from "./model.js";
5
+ // Every narration piece brought to the one common loudness before the join (`levelFile`), each
6
+ // written again as a WAV in `directory` for the join to read in their place. They come out in one format (44.1 kHz mono, which is what the
7
+ // multi-voice join already brings every turn to), so the plain concat can join them too. A piece
8
+ // too short or quiet to measure keeps its own level. Each levelled piece is measured again, so
9
+ // the report says what the levelling did, not what it meant to do.
10
+ const format = { sampleRate: 44100, channels: 1 };
11
+ const goal = { lufs: pieceLufs, truePeak: pieceTruePeak };
12
+ export async function levelPieces(run, files, directory) {
13
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
14
+ const levelled = [];
15
+ const before = [];
16
+ const after = [];
17
+ let skipped = 0;
18
+ for (const [index, file] of files.entries()) {
19
+ run.signal.throwIfAborted();
20
+ const output = join(directory, `piece-${String(index + 1).padStart(4, "0")}.wav`);
21
+ const measured = await levelFile(run, file, output, goal, format);
22
+ levelled.push(output);
23
+ if (!measurable(measured.before)) {
24
+ skipped += 1;
25
+ continue;
26
+ }
27
+ before.push(measured.before.integrated);
28
+ if (measurable(measured.after))
29
+ after.push(measured.after.integrated);
30
+ }
31
+ return {
32
+ files: levelled,
33
+ report: {
34
+ pieces: files.length,
35
+ skipped,
36
+ spreadBefore: spreadOf(before),
37
+ spreadAfter: spreadOf(after),
38
+ target: pieceLufs,
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,164 @@
1
+ import { renameSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { runFfmpeg } from "../video/ffmpeg.js";
4
+ // ceiling: below -70 LUFS (the filter's absolute gate) or under 0.4 s (one measuring block)
5
+ // there is nothing to measure: a piece that quiet or short is left as it is.
6
+ const silentBelow = -70;
7
+ const measuredJson = z.object({
8
+ input_i: z.string(),
9
+ input_tp: z.string(),
10
+ input_lra: z.string(),
11
+ input_thresh: z.string(),
12
+ target_offset: z.string(),
13
+ });
14
+ function measureFilter(goal) {
15
+ return `loudnorm=I=${goal.lufs.toFixed(1)}:TP=${goal.truePeak.toFixed(1)}:LRA=11:print_format=json`;
16
+ }
17
+ // The first pass: decode, measure, write nothing. `input` is ffmpeg input arguments, so a
18
+ // filter graph (the video's mix) can be measured as well as a file.
19
+ export function measureArgs(input, goal, chain = "") {
20
+ return [
21
+ "-hide_banner",
22
+ "-nostdin",
23
+ // loudnorm prints its JSON at the info level.
24
+ "-loglevel",
25
+ "info",
26
+ "-nostats",
27
+ ...input,
28
+ "-af",
29
+ `${chain === "" ? "" : `${chain},`}${measureFilter(goal)}`,
30
+ "-f",
31
+ "null",
32
+ "-",
33
+ ];
34
+ }
35
+ // The second pass: the gain that lands the file on the level, the peak limiter where the gain
36
+ // would push the peaks over the ceiling, and the file's sample rate. Unity for a file there was
37
+ // nothing to measure in.
38
+ export function gainFilter(goal, measured, sampleRate) {
39
+ const resample = `aresample=${String(sampleRate)}`;
40
+ if (measured === undefined || !measurable(measured))
41
+ return resample;
42
+ const gain = goal.lufs - measured.integrated;
43
+ const limit = Number.isFinite(measured.truePeak) && measured.truePeak + gain > goal.truePeak
44
+ ? `,aresample=192000,alimiter=limit=${(10 ** (goal.truePeak / 20)).toFixed(4)}:attack=5:release=50:level=false`
45
+ : "";
46
+ return `volume=${gain.toFixed(2)}dB${limit},${resample}`;
47
+ }
48
+ export function measurable(measured) {
49
+ return Number.isFinite(measured.integrated) && measured.integrated > silentBelow;
50
+ }
51
+ // The last JSON block loudnorm printed. Its numbers are strings, and "-inf" for silence.
52
+ export function parseMeasured(stderr) {
53
+ const start = stderr.lastIndexOf("{");
54
+ const end = stderr.lastIndexOf("}");
55
+ if (start < 0 || end < start)
56
+ return undefined;
57
+ let raw;
58
+ try {
59
+ raw = JSON.parse(stderr.slice(start, end + 1));
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ const parsed = measuredJson.safeParse(raw);
65
+ if (!parsed.success)
66
+ return undefined;
67
+ const number = (text) => {
68
+ const value = Number(text);
69
+ return Number.isNaN(value) ? Number.NEGATIVE_INFINITY : value;
70
+ };
71
+ return {
72
+ integrated: number(parsed.data.input_i),
73
+ truePeak: number(parsed.data.input_tp),
74
+ range: number(parsed.data.input_lra),
75
+ threshold: number(parsed.data.input_thresh),
76
+ offset: number(parsed.data.target_offset),
77
+ };
78
+ }
79
+ // Measure `input` (ffmpeg input arguments, `chain` an optional filter before the measurement).
80
+ export async function measure(run, input, goal, chain = "") {
81
+ let stderr = "";
82
+ await runFfmpeg({
83
+ bin: run.bin,
84
+ args: measureArgs(input, goal, chain),
85
+ signal: run.signal,
86
+ log: run.log,
87
+ onProgress: () => { },
88
+ onStderr: (text) => {
89
+ stderr += text;
90
+ },
91
+ });
92
+ const measured = parseMeasured(stderr);
93
+ if (measured === undefined)
94
+ throw new Error("Slopify couldn't measure the loudness of the narration (ffmpeg printed no measurement). Try again; if it happens again, turn off Level the volume in Edit project → Pauses and volume, then use Download diagnostics in Settings and report it.");
95
+ return measured;
96
+ }
97
+ export async function measureFile(run, path, goal) {
98
+ return measure(run, ["-i", path], goal);
99
+ }
100
+ // One file measured and written again at `goal`, as WAV at `sampleRate` with `channels`
101
+ // channels (32-bit float, so nothing clips between here and the encode). Returns what the first
102
+ // pass measured.
103
+ export async function normalizeFile(run, input, output, goal, format) {
104
+ // The channels are set before anything is measured: a stereo file mixed down to mono is
105
+ // louder per channel (the mix keeps its loudness, so each sample is about 3 dB higher), and
106
+ // the gain and the peak ceiling have to hold for what is written.
107
+ const channels = `aformat=channel_layouts=${format.channels === 1 ? "mono" : "stereo"}`;
108
+ const measured = await measure(run, ["-i", input], goal, channels);
109
+ await runFfmpeg({
110
+ bin: run.bin,
111
+ args: [
112
+ "-hide_banner",
113
+ "-nostdin",
114
+ "-loglevel",
115
+ "error",
116
+ "-nostats",
117
+ "-y",
118
+ "-i",
119
+ input,
120
+ "-map",
121
+ "0:a:0",
122
+ "-af",
123
+ `${channels},${gainFilter(goal, measured, format.sampleRate)}`,
124
+ "-c:a",
125
+ "pcm_f32le",
126
+ "-f",
127
+ "wav",
128
+ output,
129
+ ],
130
+ signal: run.signal,
131
+ log: run.log,
132
+ onProgress: () => { },
133
+ });
134
+ return measured;
135
+ }
136
+ // ceiling: within a fifth of a LU of the level no one hears the difference.
137
+ const correctAbove = 0.2;
138
+ // One file brought to `goal` (`normalizeFile`), measured again, and corrected once when the
139
+ // limiter left it more than a fifth of a LU under the level. Returns the first measurement and
140
+ // the last.
141
+ export async function levelFile(run, input, output, goal, format) {
142
+ const before = await normalizeFile(run, input, output, goal, format);
143
+ let after = await measureFile(run, output, goal);
144
+ if (measurable(after) && Math.abs(after.integrated - goal.lufs) > correctAbove) {
145
+ const corrected = `${output}.corrected.wav`;
146
+ await normalizeFile(run, output, corrected, goal, format);
147
+ renameSync(corrected, output);
148
+ after = await measureFile(run, output, goal);
149
+ }
150
+ return { before, after };
151
+ }
152
+ // A finished file's sound brought to its master target.
153
+ export async function masterFile(run, input, output, goal, format) {
154
+ await levelFile(run, input, output, goal, format);
155
+ }
156
+ // What a finished file measures, for its output (`MasterReport`).
157
+ export async function masterReport(run, file, goal) {
158
+ const check = await measureFile(run, file, goal);
159
+ return {
160
+ target: goal.lufs,
161
+ integrated: Math.round(check.integrated * 10) / 10,
162
+ truePeak: Math.round(check.truePeak * 10) / 10,
163
+ };
164
+ }
@@ -0,0 +1,153 @@
1
+ // Loudness normalization ("Level the volume"). Text-to-speech comes back at a different
2
+ // loudness per voice and even per request, so a joined narration jumps. With the setting on:
3
+ // - every narration piece (a chunk, an intro or outro part, a speaker's turn) is measured and
4
+ // brought to one common loudness before the pieces are joined (`loudnorm.ts`), in a step of
5
+ // its own beside the plain join (`recipe-audio.ts`), so the word timing, which reads the
6
+ // plain join, and everything written from it are never made again for a volume change;
7
+ // - every finished file is mastered to a target: the long video and the shorts to the video
8
+ // target, the audio files (the audio-only WAV, an audiobook's MP3 and M4B) to their own.
9
+ // The ambient bed and the shorts' music are mixed before the master and ducked by the levelled
10
+ // voice, so they keep their place under it.
11
+ //
12
+ // Browser-safe: Play, Edit project and Settings convert the same numbers.
13
+ //
14
+ // A run without the setting has no `loudness` at all, which is what every project saved before
15
+ // it was, and every recipe leaves it out of its fingerprint then.
16
+ // YouTube and Spotify play everything back at about -14 LUFS, so a video mastered there is
17
+ // neither turned down nor left quiet. Audiobook shops ask for about -18 to -20 LUFS with more
18
+ // headroom, so the listening files sit lower.
19
+ export const recommendedVideoLufs = -14;
20
+ export const recommendedAudioFilesLufs = -18;
21
+ export const defaultLoudness = {
22
+ enabled: true,
23
+ videoLufs: recommendedVideoLufs,
24
+ audioFilesLufs: recommendedAudioFilesLufs,
25
+ };
26
+ // The peak ceilings, in dBTP (true peak, between the samples). The video's leaves room for the
27
+ // lossy AAC encode; the audio files' is what audiobook shops ask for.
28
+ export const videoTruePeak = -1.5;
29
+ export const audioFilesTruePeak = -3;
30
+ // The master aims this far under the ceiling: a lossy encode (the video's AAC, the MP3 and
31
+ // M4B) lifts the peaks by up to about 0.7 dB at the bitrates Slopify writes, and what is
32
+ // measured is the finished file.
33
+ export const encodeHeadroom = 1;
34
+ // The common level every narration piece is brought to before the join. Fixed, so changing the
35
+ // Volume only masters the finished files again and never re-joins the narration. ceiling: a
36
+ // voice's peaks run 14 to 18 dB over its loudness, so -20 LUFS leaves room for nearly all of
37
+ // them under -2 dBTP; the master then lifts the whole to the target.
38
+ export const pieceLufs = -20;
39
+ export const pieceTruePeak = -2;
40
+ // The Volume control is in dB from the recommended level: 0 dB is the recommendation. Below
41
+ // -10 dB a video sounds broken next to others; above +4 dB the peak ceiling squashes the voice.
42
+ export const loudnessDbMin = -10;
43
+ export const loudnessDbMax = 4;
44
+ export const loudnessDbStep = 0.5;
45
+ // The master a run's file of this kind gets, or undefined while the setting is off.
46
+ export function masterGoal(config, target) {
47
+ const settings = config.loudness;
48
+ if (settings === undefined || !usesLoudness(config))
49
+ return undefined;
50
+ return {
51
+ lufs: target === "video" ? settings.videoLufs : settings.audioFilesLufs,
52
+ truePeak: truePeakOf(target) - encodeHeadroom,
53
+ };
54
+ }
55
+ export function recommendedLufs(target) {
56
+ return target === "video" ? recommendedVideoLufs : recommendedAudioFilesLufs;
57
+ }
58
+ export function truePeakOf(target) {
59
+ return target === "video" ? videoTruePeak : audioFilesTruePeak;
60
+ }
61
+ // dB from the recommended level, and back. Stored is the LUFS; the dB is what is shown.
62
+ export function dbOfLufs(lufs, target) {
63
+ return round(lufs - recommendedLufs(target), 2);
64
+ }
65
+ export function lufsOfDb(db, target) {
66
+ return round(recommendedLufs(target) + db, 2);
67
+ }
68
+ // Loudness as a share of the recommended level, the way a volume knob reads: -6 dB is about
69
+ // half as loud, +6 dB about twice. An amplitude ratio, so it matches what the gain does.
70
+ export function percentOfDb(db) {
71
+ return Math.round(10 ** (db / 20) * 100);
72
+ }
73
+ export function dbOfPercent(percent) {
74
+ return round(20 * Math.log10(percent / 100), 2);
75
+ }
76
+ // Typed dB snapped to the control's step and range; undefined for text that is not a number.
77
+ export function snapDb(db) {
78
+ if (!Number.isFinite(db))
79
+ return undefined;
80
+ const snapped = Math.round(db / loudnessDbStep) * loudnessDbStep;
81
+ return Math.min(loudnessDbMax, Math.max(loudnessDbMin, snapped));
82
+ }
83
+ export function loudnessProblem(lufs, target) {
84
+ const db = lufs - recommendedLufs(target);
85
+ return Number.isFinite(db) &&
86
+ Number.isInteger(round(db / loudnessDbStep, 6)) &&
87
+ db >= loudnessDbMin - 1e-9 &&
88
+ db <= loudnessDbMax + 1e-9
89
+ ? undefined
90
+ : `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (Play → Video and style, Edit project → Pauses and volume, or Settings → General).`;
91
+ }
92
+ export function loudnessFields(settings) {
93
+ if (settings === undefined)
94
+ return [];
95
+ const fields = [];
96
+ const video = loudnessProblem(settings.videoLufs, "video");
97
+ if (video !== undefined)
98
+ fields.push({ field: "loudness.videoLufs", message: video });
99
+ const files = loudnessProblem(settings.audioFilesLufs, "audioFiles");
100
+ if (files !== undefined)
101
+ fields.push({ field: "loudness.audioFilesLufs", message: files });
102
+ return fields;
103
+ }
104
+ // Whether a run levels and masters its sound: the setting is on and there is a narration.
105
+ export function usesLoudness(config) {
106
+ return config.loudness !== undefined && config.sources.audio !== "off";
107
+ }
108
+ // The settings for a new run from the app-wide default; undefined when it is off.
109
+ export function loudnessOfDefault(value) {
110
+ return value.enabled
111
+ ? { videoLufs: value.videoLufs, audioFilesLufs: value.audioFilesLufs }
112
+ : undefined;
113
+ }
114
+ export function loudnessOfForm(form, fallback) {
115
+ if (!(form?.enabled ?? fallback.enabled))
116
+ return undefined;
117
+ return {
118
+ videoLufs: form?.videoLufs ?? fallback.videoLufs,
119
+ audioFilesLufs: form?.audioFilesLufs ?? fallback.audioFilesLufs,
120
+ };
121
+ }
122
+ // "0 dB (recommended, -14 LUFS)", "-6 dB (50%, -20 LUFS)".
123
+ export function loudnessLabel(lufs, target) {
124
+ const db = dbOfLufs(lufs, target);
125
+ const lufsText = `${formatNumber(lufs)} LUFS`;
126
+ if (db === 0)
127
+ return `0 dB (recommended, ${lufsText})`;
128
+ return `${db > 0 ? "+" : ""}${formatNumber(db)} dB (${String(percentOfDb(db))}%, ${lufsText})`;
129
+ }
130
+ // The loudest minus the quietest; 0 for fewer than two.
131
+ export function spreadOf(values) {
132
+ if (values.length < 2)
133
+ return 0;
134
+ return round(Math.max(...values) - Math.min(...values), 1);
135
+ }
136
+ export function reportText(report) {
137
+ const pieces = `${String(report.pieces)} ${report.pieces === 1 ? "piece" : "pieces"}`;
138
+ const skipped = report.skipped === 0
139
+ ? ""
140
+ : ` (${String(report.skipped)} too short or quiet to measure, left as they were)`;
141
+ return `Levelled ${pieces} to ${formatNumber(report.target)} LUFS${skipped}: the spread was ${formatNumber(report.spreadBefore)} LU, now ${formatNumber(report.spreadAfter)} LU.`;
142
+ }
143
+ // "Mastered to -14 LUFS: measured -14.1 LUFS, peaks -1.6 dBTP".
144
+ export function masterText(report) {
145
+ return `Mastered to ${formatNumber(report.target)} LUFS: measured ${formatNumber(report.integrated)} LUFS, peaks ${formatNumber(report.truePeak)} dBTP`;
146
+ }
147
+ function round(value, places) {
148
+ const scale = 10 ** places;
149
+ return Math.round(value * scale) / scale;
150
+ }
151
+ function formatNumber(value) {
152
+ return String(round(value, 1)).replace("-", "−");
153
+ }
@@ -0,0 +1,10 @@
1
+ // The model-upkeep vocabulary, free of the database so the browser can import it too.
2
+ // The four model choices a run makes, and the catalogue list each one comes from.
3
+ export const usageSlots = ["llm", "audio", "images", "animate"];
4
+ export const usageKinds = ["template", "schedule", "draft", "project"];
5
+ export const slotLabels = {
6
+ llm: "Text model",
7
+ audio: "Voice model",
8
+ images: "Image model",
9
+ animate: "Animation model",
10
+ };