@gentbajko/slopify 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +11 -1
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -20,9 +20,20 @@ export function chunkNarration(text, chunking) {
20
20
  }
21
21
  }
22
22
  // What "N words" counts: runs of non-space. Exported because it is half of the rule -
23
- // a test that asserts a chunk fits in N has to count the same way the cut did.
23
+ // a test that asserts a chunk fits in N has to count the same way the cut did. Chinese,
24
+ // Japanese, Thai and the other scripts written without spaces are counted by the platform's
25
+ // own word breaker instead, or a whole paragraph would be one "word"; text without them
26
+ // (every English narration) is counted exactly as before.
27
+ const unspaced = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]/u;
24
28
  export function wordsIn(text) {
25
- return text.split(/\s+/).filter((word) => word !== "").length;
29
+ if (!unspaced.test(text))
30
+ return text.split(/\s+/).filter((word) => word !== "").length;
31
+ const segmenter = new Intl.Segmenter("en", { granularity: "word" });
32
+ let count = 0;
33
+ for (const part of segmenter.segment(text))
34
+ if (part.isWordLike === true)
35
+ count += 1;
36
+ return count;
26
37
  }
27
38
  export function sameChunking(left, right) {
28
39
  const a = left ?? defaultChunking;
@@ -52,11 +63,12 @@ function sentenceRuns(text, budget, measure) {
52
63
  // The provider-specific planning layer applies any hard request limit afterwards.
53
64
  return nonEmpty(chunks);
54
65
  }
55
- // ceiling: segmented as English. `Intl.Segmenter` is the platform's own sentence breaker
56
- // (ICU ships with Node), so no dependency and no regex full of abbreviations; the locale
57
- // is fixed because a run carries no language and the default one varies by machine. The
58
- // upgrade is a language on the run configuration, passed through to here.
59
- function* sentences(text) {
66
+ // `Intl.Segmenter` is the platform's own sentence breaker (ICU ships with Node), so no
67
+ // dependency and no regex full of abbreviations. Its sentence rules are Unicode's (UAX #29)
68
+ // for every language: "。", "!", "।", "؟" and "¿…?" end or open sentences whatever the
69
+ // locale, so the project language needs no locale here, and "en" keeps it the same on every
70
+ // machine (the default locale varies).
71
+ export function* sentences(text) {
60
72
  // Built per call rather than held at module scope: the standards forbid a module-level
61
73
  // singleton, and constructing one costs microseconds against a call that just read an
62
74
  // article off disk.
@@ -46,10 +46,10 @@ export async function joinNarration(deps, input) {
46
46
  const first = input.files[0];
47
47
  if (first === undefined) {
48
48
  // The stage refuses an empty narration before it gets here, so this is a bug.
49
- throw new Error("Slopify hit an internal error (there is no narration audio to join). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
49
+ throw new Error("Slopify hit an internal error (there is no narration audio to join). Try again; if it happens again, use Download diagnostics in Settings and report it.");
50
50
  }
51
51
  mkdirSync(dirname(input.output), { recursive: true, mode: 0o700 });
52
- if (input.files.length === 1) {
52
+ if (input.files.length === 1 && input.reencode !== true) {
53
53
  // Whole-text mode, and any run whose article is one paragraph: there is nothing to
54
54
  // join, and re-encoding the only chunk would lose quality for no reason.
55
55
  input.signal.throwIfAborted();
@@ -0,0 +1,45 @@
1
+ import { withLanguage } from "../../kernel/ports/languages.js";
2
+ const instructions = {
3
+ table: "Describe this table for a listener in 1–4 sentences in the narration's language and tone; give the pattern and the notable values, not every cell; never read it row by row.",
4
+ figure: "Describe this figure for a listener in 1–3 sentences in the narration's language and tone, from its caption, alt text and legend (and the attached picture, when there is one): say what it shows and what to take from it. Never say that a figure shows something without saying what; explain colours or symbols only through what they stand for.",
5
+ diagram: "Describe this diagram or chart for a listener in 1–3 sentences in the narration's language and tone: what it connects or compares and what it tells us. Never read labels, arrows or axis ticks one by one.",
6
+ math: "Say what this equation means for a listener in 1–3 sentences in the narration's language and tone: how the quantities depend on each other (for example, “evaporation grows with temperature and falls with humidity”), not symbol by symbol. A single short symbol may be read as it is.",
7
+ code: "Summarise what this code does for a listener in one or two sentences in the narration's language and tone, naming the language when it helps (for example, “a Rust function that carves river channels by lowering the terrain along the flow”). Never read code, symbols or names of variables aloud.",
8
+ };
9
+ const inlineMath = "Rewrite this paragraph so it can be read aloud, in the narration's language and tone: keep its words, but say each formula's meaning in words rather than symbol by symbol. A single short symbol may be read as it is.";
10
+ const answerRules = "Answer with the spoken passage only: plain sentences a narrator reads out, no Markdown, no lists, no headings, no quotation marks around the whole answer, no preamble such as “Here is”.";
11
+ export function describeInstruction(block) {
12
+ return block.kind === "math" && block.inline === true ? inlineMath : instructions[block.kind];
13
+ }
14
+ export function describeMessages(block, style, language) {
15
+ const guidance = style.trim();
16
+ const system = [
17
+ describeInstruction(block),
18
+ answerRules,
19
+ ...(guidance === ""
20
+ ? []
21
+ : [
22
+ `Style guidance for the narration (follow its tone; ignore anything about formats or cues):\n${guidance}`,
23
+ ]),
24
+ ].join("\n\n");
25
+ const user = [
26
+ ...(block.section === null ? [] : [`Section: ${block.section}`]),
27
+ ...(block.lang === undefined ? [] : [`Language of the code: ${block.lang}`]),
28
+ block.source,
29
+ ].join("\n\n");
30
+ return withLanguage([
31
+ { role: "system", content: system },
32
+ { role: "user", content: user },
33
+ ], language);
34
+ }
35
+ // The passage as the narration says it: one paragraph, no Markdown a voice would read out.
36
+ export function spokenPassage(answer) {
37
+ return answer
38
+ .replace(/```[\s\S]*?```/g, " ")
39
+ .replace(/^\s*(?:[-*+]|\d+[.)]|#{1,6})\s+/gm, "")
40
+ .replace(/[*_`#|]/g, "")
41
+ .replace(/\s+/g, " ")
42
+ .trim()
43
+ .replace(/^["“](.*)["”]$/s, "$1")
44
+ .trim();
45
+ }
@@ -0,0 +1,63 @@
1
+ // Pauses between sentences. Some voices (Inworld's TTS-2 among them) run one sentence into the
2
+ // next with barely a breath, so a narration sounds rushed. With a minimum set, the join finds
3
+ // the quiet after each sentence in every narration piece and lengthens it to at least that
4
+ // long by adding silence in the middle of it (`pauses.ts`): an existing pause is never
5
+ // shortened and a word is never cut. It happens in the join, before the word timing reads the
6
+ // narration, so the captions, the word-by-word highlights, the cuts, the shorts' clips, the
7
+ // chapters and the M4B's marks are timed on the paced narration and stay in step with it.
8
+ //
9
+ // A paragraph's minimum is separate and 0 (the sentence minimum only) unless set: the pause
10
+ // between paragraphs is mostly the narration prompt's and the voice's to make.
11
+ //
12
+ // Browser-safe: Play and Edit project read these limits. A run without either has neither on
13
+ // its config, which is what every project saved before them was, and the joins keep their
14
+ // fingerprints then.
15
+ // ceiling: a quarter second reads brisk, 0.35 to 0.45 natural or documentary, 0.5 to 0.8 an
16
+ // audiobook's relaxed pace, and above 0.8 sleep content; beyond 2 s it stops sounding like a
17
+ // pause between sentences.
18
+ export const defaultSentencePauseSeconds = 0.4;
19
+ export const pauseSecondsMax = 2;
20
+ export const pauseSecondsStep = 0.05;
21
+ // What a run's config asks for; undefined when neither minimum is set.
22
+ export function pausesOf(config) {
23
+ const sentenceSeconds = config.sentencePauseSeconds ?? 0;
24
+ const paragraphSeconds = config.paragraphPauseSeconds ?? 0;
25
+ return sentenceSeconds > 0 || paragraphSeconds > 0
26
+ ? { sentenceSeconds, paragraphSeconds }
27
+ : undefined;
28
+ }
29
+ export function pauseProblem(value, name) {
30
+ const steps = value / pauseSecondsStep;
31
+ return Number.isFinite(value) &&
32
+ value >= 0 &&
33
+ value <= pauseSecondsMax &&
34
+ Math.abs(steps - Math.round(steps)) < 1e-6
35
+ ? undefined
36
+ : `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under Play → Outputs → Export, or Edit project → Pauses and volume.`;
37
+ }
38
+ export function pauseFields(config) {
39
+ const fields = [];
40
+ if (config.sentencePauseSeconds !== undefined) {
41
+ const problem = pauseProblem(config.sentencePauseSeconds, "sentences");
42
+ if (problem !== undefined)
43
+ fields.push({ field: "sentencePauseSeconds", message: problem });
44
+ }
45
+ if (config.paragraphPauseSeconds !== undefined) {
46
+ const problem = pauseProblem(config.paragraphPauseSeconds, "paragraphs");
47
+ if (problem !== undefined)
48
+ fields.push({ field: "paragraphPauseSeconds", message: problem });
49
+ }
50
+ return fields;
51
+ }
52
+ // Play's typed pauses as a run's config holds them: absent text is the default (the sentence
53
+ // default, no paragraph minimum), a value of 0 is left off, and text that is not a number is
54
+ // NaN, which the rule refuses in place.
55
+ export function pausesOfForm(form) {
56
+ const typed = (raw, fallback) => raw === undefined || raw.trim() === "" ? fallback : Number(raw.trim().replace(",", "."));
57
+ const sentence = typed(form.sentencePause, defaultSentencePauseSeconds);
58
+ const paragraph = typed(form.paragraphPause, 0);
59
+ return {
60
+ ...(sentence === 0 ? {} : { sentencePauseSeconds: sentence }),
61
+ ...(paragraph === 0 ? {} : { paragraphPauseSeconds: paragraph }),
62
+ };
63
+ }
@@ -0,0 +1,269 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { measureFile } from "../loudness/loudnorm.js";
4
+ import { pieceLufs, pieceTruePeak } from "../loudness/model.js";
5
+ import { probeDurationMs, runFfmpeg } from "../video/ffmpeg.js";
6
+ import { sentences } from "./chunk.js";
7
+ // The sentence ends inside a text, not counting its last.
8
+ export function textBoundaries(text) {
9
+ const parts = [...sentences(text)];
10
+ const total = Array.from(text).length;
11
+ if (parts.length < 2 || total === 0)
12
+ return [];
13
+ const boundaries = [];
14
+ let offset = 0;
15
+ for (const part of parts.slice(0, -1)) {
16
+ offset += Array.from(part).length;
17
+ const trailing = /\s*$/.exec(part)?.[0] ?? "";
18
+ // Only a real sentence: text that is only spacing is no sentence of its own.
19
+ if (part.trim() === "")
20
+ continue;
21
+ boundaries.push({
22
+ ratio: offset / total,
23
+ kind: /\n[ \t\r]*\n/.test(trailing) || /\n/.test(trailing) ? "paragraph" : "sentence",
24
+ });
25
+ }
26
+ return boundaries;
27
+ }
28
+ // silencedetect prints `silence_start: 1.23` and `silence_end: 1.56 | silence_duration: 0.33`.
29
+ // A stretch still open at the end runs to the end of the piece.
30
+ export function parseSilences(stderr, durationSeconds) {
31
+ const silences = [];
32
+ let open;
33
+ for (const match of stderr.matchAll(/silence_(start|end):\s*(-?[0-9.]+)/g)) {
34
+ const value = Math.max(0, Number(match[2]));
35
+ if (!Number.isFinite(value))
36
+ continue;
37
+ if (match[1] === "start")
38
+ open = value;
39
+ else if (open !== undefined) {
40
+ silences.push({ start: open, end: Math.min(durationSeconds, value) });
41
+ open = undefined;
42
+ }
43
+ }
44
+ if (open !== undefined && open < durationSeconds)
45
+ silences.push({ start: open, end: durationSeconds });
46
+ return silences;
47
+ }
48
+ // ceiling: a sentence end more than two seconds (or a fifth of the piece) from where its words
49
+ // put it is not trusted to be that sentence's pause.
50
+ const windowSeconds = 2;
51
+ const windowShare = 0.2;
52
+ // ceiling: a second of quiet counts for three seconds of distance, so a full stop's pause wins
53
+ // over a comma's breath a little nearer to where the words put the end.
54
+ const durationWeight = 3;
55
+ // A stretch this near either end of the piece is the piece's own lead-in or tail.
56
+ const edgeSeconds = 0.02;
57
+ export function planPauses(input) {
58
+ const { durationSeconds, silences, boundaries, settings } = input;
59
+ const leadingSilence = silences.find((one) => one.start <= edgeSeconds);
60
+ const trailingSilence = silences.find((one) => one.end >= durationSeconds - edgeSeconds);
61
+ const leading = leadingSilence === undefined ? 0 : leadingSilence.end;
62
+ const trailing = trailingSilence === undefined || trailingSilence === leadingSilence
63
+ ? 0
64
+ : durationSeconds - trailingSilence.start;
65
+ const speechStart = leading;
66
+ const speechEnd = Math.max(speechStart, durationSeconds - trailing);
67
+ const speech = speechEnd - speechStart;
68
+ const inner = silences.filter((one) => one !== leadingSilence && one !== trailingSilence);
69
+ if (speech <= 0 || inner.length === 0 || boundaries.length === 0)
70
+ return { inserts: [], leading, trailing };
71
+ const window = Math.max(windowSeconds, speech * windowShare);
72
+ const expected = boundaries.map((one) => speechStart + one.ratio * speech);
73
+ // The best in-order matching: every sentence end takes a later stretch than the one before,
74
+ // or none. Cost is the distance from where the words put the end, less the stretch's length
75
+ // weighted (a full stop's pause is longer than a comma's); leaving an end unmatched costs the
76
+ // window.
77
+ const m = boundaries.length;
78
+ const n = inner.length;
79
+ const cost = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => Number.POSITIVE_INFINITY));
80
+ const choice = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => "skip-end"));
81
+ for (let j = 0; j <= n; j += 1) {
82
+ const row = cost[0];
83
+ if (row !== undefined)
84
+ row[j] = 0;
85
+ }
86
+ for (let i = 1; i <= m; i += 1) {
87
+ const row = cost[i];
88
+ const previous = cost[i - 1];
89
+ const picks = choice[i];
90
+ row[0] = (previous[0] ?? 0) + window;
91
+ picks[0] = "skip-end";
92
+ for (let j = 1; j <= n; j += 1) {
93
+ const silence = inner[j - 1];
94
+ const middle = (silence.start + silence.end) / 2;
95
+ const distance = Math.abs(middle - (expected[i - 1] ?? 0));
96
+ const options = [
97
+ [(previous[j] ?? 0) + window, "skip-end"],
98
+ [row[j - 1] ?? Number.POSITIVE_INFINITY, "skip-silence"],
99
+ ];
100
+ if (distance <= window)
101
+ options.push([
102
+ (previous[j - 1] ?? 0) + distance - durationWeight * (silence.end - silence.start),
103
+ "match",
104
+ ]);
105
+ const best = options.reduce((low, one) => (one[0] < low[0] ? one : low));
106
+ row[j] = best[0];
107
+ picks[j] = best[1];
108
+ }
109
+ }
110
+ const matched = [];
111
+ let i = m;
112
+ let j = n;
113
+ while (i > 0) {
114
+ const pick = choice[i]?.[j] ?? "skip-end";
115
+ if (j > 0 && pick === "match") {
116
+ matched.push({
117
+ boundary: boundaries[i - 1],
118
+ silence: inner[j - 1],
119
+ });
120
+ i -= 1;
121
+ j -= 1;
122
+ }
123
+ else if (j > 0 && pick === "skip-silence")
124
+ j -= 1;
125
+ else
126
+ i -= 1;
127
+ }
128
+ const inserts = matched.reverse().flatMap(({ boundary, silence }) => {
129
+ const wanted = minimumFor(boundary.kind, settings);
130
+ const have = silence.end - silence.start;
131
+ return wanted > have
132
+ ? [{ at: round((silence.start + silence.end) / 2), seconds: round(wanted - have) }]
133
+ : [];
134
+ });
135
+ return { inserts, leading, trailing };
136
+ }
137
+ export function minimumFor(kind, settings) {
138
+ return kind === "paragraph"
139
+ ? Math.max(settings.sentenceSeconds, settings.paragraphSeconds)
140
+ : settings.sentenceSeconds;
141
+ }
142
+ // The piece with its silences added and `padEnd` seconds of silence after it, as 16-bit PCM
143
+ // WAV. Every stretch between two inserts is cut out of the piece as it is and played in order.
144
+ export function insertArgs(input, output, inserts, padEnd, format) {
145
+ const layout = format.channels === 1 ? "mono" : "stereo";
146
+ const base = `aformat=sample_fmts=fltp:sample_rates=${String(format.sampleRate)}:channel_layouts=${layout}`;
147
+ const chains = [
148
+ `[0:a]${base},asplit=${String(inserts.length + 1)}${inserts.map((_one, at) => `[s${String(at)}]`).join("")}[s${String(inserts.length)}]`,
149
+ ];
150
+ const cuts = [0, ...inserts.map((one) => one.at)];
151
+ cuts.forEach((from, at) => {
152
+ const to = inserts[at]?.at;
153
+ const trim = `atrim=start=${from.toFixed(6)}${to === undefined ? "" : `:end=${to.toFixed(6)}`},asetpts=PTS-STARTPTS`;
154
+ const before = at === 0 ? undefined : inserts[at - 1]?.seconds;
155
+ const delay = before === undefined ? "" : `,adelay=delays=${Math.round(before * 1000)}:all=1`;
156
+ const pad = to === undefined && padEnd > 0 ? `,apad=pad_dur=${padEnd.toFixed(3)}` : "";
157
+ chains.push(`[s${String(at)}]${trim}${delay}${pad}[p${String(at)}]`);
158
+ });
159
+ chains.push(`${cuts.map((_one, at) => `[p${String(at)}]`).join("")}concat=n=${String(cuts.length)}:v=0:a=1[a]`);
160
+ return [
161
+ "-hide_banner",
162
+ "-nostdin",
163
+ "-loglevel",
164
+ "error",
165
+ "-nostats",
166
+ "-y",
167
+ "-i",
168
+ input,
169
+ "-filter_complex",
170
+ chains.join(";"),
171
+ "-map",
172
+ "[a]",
173
+ "-c:a",
174
+ "pcm_s16le",
175
+ "-f",
176
+ "wav",
177
+ output,
178
+ ];
179
+ }
180
+ // ceiling: quiet is 22 dB under the piece's own loudness (never above -35 dBFS), held for at
181
+ // least 60 ms: under a sentence's breath, over a room's hiss.
182
+ const quietBelow = 22;
183
+ const quietFloor = -35;
184
+ const quietSeconds = 0.06;
185
+ // Where a piece is quiet, measured on the piece as spoken.
186
+ export async function measurePiece(run, file) {
187
+ const durationSeconds = (await probeDurationMs(run.bin, file, run.signal, run.log)) / 1000;
188
+ const loudness = await measureFile(run, file, { lufs: pieceLufs, truePeak: pieceTruePeak });
189
+ const noise = Number.isFinite(loudness.integrated)
190
+ ? Math.min(quietFloor, loudness.integrated - quietBelow)
191
+ : quietFloor;
192
+ let stderr = "";
193
+ await runFfmpeg({
194
+ bin: run.bin,
195
+ args: [
196
+ "-hide_banner",
197
+ "-nostdin",
198
+ "-loglevel",
199
+ "info",
200
+ "-nostats",
201
+ "-i",
202
+ file,
203
+ "-af",
204
+ `silencedetect=noise=${noise.toFixed(1)}dB:d=${String(quietSeconds)}`,
205
+ "-f",
206
+ "null",
207
+ "-",
208
+ ],
209
+ signal: run.signal,
210
+ log: run.log,
211
+ onProgress: () => { },
212
+ onStderr: (text) => {
213
+ stderr += text;
214
+ },
215
+ });
216
+ return { durationSeconds, silences: parseSilences(stderr, durationSeconds) };
217
+ }
218
+ // Every piece's plan, from the pieces as spoken: silences inside, and at each piece's end what
219
+ // the gap to the next piece lacks.
220
+ export async function planPieces(run, pieces, settings) {
221
+ const measured = [];
222
+ for (const piece of pieces) {
223
+ run.signal.throwIfAborted();
224
+ const { durationSeconds, silences } = await measurePiece(run, piece.file);
225
+ measured.push(planPauses({
226
+ durationSeconds,
227
+ silences,
228
+ boundaries: textBoundaries(piece.text),
229
+ settings,
230
+ }));
231
+ }
232
+ return measured.map((pauses, at) => {
233
+ const piece = pieces[at];
234
+ const following = measured[at + 1];
235
+ if (piece.next === "end" || piece.next === "turn" || following === undefined)
236
+ return { pauses, padEnd: 0 };
237
+ const gap = pauses.trailing + following.leading;
238
+ return { pauses, padEnd: round(Math.max(0, minimumFor(piece.next, settings) - gap)) };
239
+ });
240
+ }
241
+ // The pieces written again with their pauses, as WAVs in `directory`, in the order given.
242
+ // `files` are what is joined (the spoken pieces, or their levelled copies); `plans` are from
243
+ // the spoken pieces.
244
+ export async function applyPauses(run, files, plans, directory) {
245
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
246
+ const written = [];
247
+ let added = 0;
248
+ for (const [index, file] of files.entries()) {
249
+ run.signal.throwIfAborted();
250
+ const plan = plans[index];
251
+ const output = join(directory, `paced-${String(index + 1).padStart(4, "0")}.wav`);
252
+ await runFfmpeg({
253
+ bin: run.bin,
254
+ args: insertArgs(file, output, plan?.pauses.inserts ?? [], plan?.padEnd ?? 0, {
255
+ sampleRate: 44100,
256
+ channels: 1,
257
+ }),
258
+ signal: run.signal,
259
+ log: run.log,
260
+ onProgress: () => { },
261
+ });
262
+ added += (plan?.pauses.inserts.length ?? 0) + ((plan?.padEnd ?? 0) > 0 ? 1 : 0);
263
+ written.push(output);
264
+ }
265
+ return { files: written, added };
266
+ }
267
+ function round(value) {
268
+ return Math.round(value * 1000) / 1000;
269
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { execFile } from "node:child_process";
2
+ // The live view's narration waveform: each finished narration piece as a row of peaks, in the
3
+ // order it is spoken, growing as pieces land. Once the whole narration is joined (or was
4
+ // uploaded) that one file replaces the pieces.
5
+ // ceiling: ten bars a second draws a two-minute short in 1200 bars and a two-hour video in
6
+ // 72000, which the browser scales down to the width it has.
7
+ export const peaksPerSecond = 10;
8
+ // The rate the audio is decoded at for peaks only; loudness needs no more than this.
9
+ const decodeRate = 4000;
10
+ // ceiling: three hours of narration at the decode rate, 16-bit mono.
11
+ const decodeMaxBytes = 3 * 3600 * decodeRate * 2;
12
+ // The saved narration audio of a revision, in spoken order: the joined narration when it is
13
+ // ready, else each finished request of the intro, the body and the outro.
14
+ export function narrationAudio(view) {
15
+ const joined = view.outputs.find((row) => row.selected && row.available && row.state === "ready" && row.output.role === "audio_body");
16
+ if (joined !== undefined)
17
+ return {
18
+ complete: true,
19
+ pieces: [{ key: joined.workKey, assetId: joined.assetId }],
20
+ };
21
+ const order = (key) => key.startsWith("audio:intro") ? 0 : key.startsWith("audio:outro") ? 2 : 1;
22
+ return {
23
+ complete: false,
24
+ pieces: view.pieces
25
+ .filter((row) => row.selected &&
26
+ row.available &&
27
+ row.stageKind === "audio" &&
28
+ row.piece.state === "done" &&
29
+ row.assetId !== null &&
30
+ row.piece.payload !== null &&
31
+ spoken(row.piece.payload))
32
+ .toSorted((left, right) => order(left.key) - order(right.key) ||
33
+ left.key.localeCompare(right.key, "en", { numeric: true }))
34
+ .flatMap((row) => (row.assetId === null ? [] : [{ key: row.key, assetId: row.assetId }])),
35
+ };
36
+ }
37
+ function spoken(payload) {
38
+ try {
39
+ const value = JSON.parse(payload);
40
+ return typeof value === "object" && value !== null && "segment" in value;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ // The loudest sample of each slice of `perSecond` a second, from 0 to 1.
47
+ export function peaksOf(samples, sampleRate, perSecond) {
48
+ const size = Math.max(1, Math.round(sampleRate / perSecond));
49
+ const peaks = [];
50
+ for (let at = 0; at < samples.length; at += size) {
51
+ let loudest = 0;
52
+ const end = Math.min(samples.length, at + size);
53
+ for (let index = at; index < end; index++) {
54
+ const value = Math.abs(samples[index] ?? 0);
55
+ if (value > loudest)
56
+ loudest = value;
57
+ }
58
+ peaks.push(Math.round((loudest / 32768) * 1000) / 1000);
59
+ }
60
+ return peaks;
61
+ }
62
+ // Decodes an audio file with ffmpeg to mono 16-bit samples and returns its peaks.
63
+ export function decodePeaks(ffmpeg, path, signal) {
64
+ return new Promise((resolve, reject) => {
65
+ execFile(ffmpeg, [
66
+ "-hide_banner",
67
+ "-nostdin",
68
+ "-loglevel",
69
+ "error",
70
+ "-i",
71
+ path,
72
+ "-ac",
73
+ "1",
74
+ "-ar",
75
+ String(decodeRate),
76
+ "-f",
77
+ "s16le",
78
+ "pipe:1",
79
+ ], { encoding: "buffer", maxBuffer: decodeMaxBytes, signal, windowsHide: true }, (error, stdout) => {
80
+ if (error !== null) {
81
+ reject(new Error(`ffmpeg could not read the narration audio (${error.message}).`));
82
+ return;
83
+ }
84
+ // Copied so the samples start on an even byte, which a pooled Buffer may not.
85
+ const bytes = new Uint8Array(stdout.byteLength - (stdout.byteLength % 2));
86
+ bytes.set(stdout.subarray(0, bytes.byteLength));
87
+ const samples = new Int16Array(bytes.buffer);
88
+ resolve({
89
+ seconds: samples.length / decodeRate,
90
+ peaks: peaksOf(samples, decodeRate, peaksPerSecond),
91
+ });
92
+ });
93
+ });
94
+ }
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { languageInfo } from "../../kernel/ports/languages.js";
3
+ import { aliasedSentences } from "./aliases.js";
2
4
  const sentence = z.number().int().positive();
3
5
  const instruction = z
4
6
  .string()
@@ -64,12 +66,28 @@ const contract = [
64
66
  "Never return or rewrite narration. Do not translate, correct, abbreviate or add dialogue.",
65
67
  "Use cues sparingly; an empty cues array is valid. Combine simultaneous directions.",
66
68
  ].join("\n");
67
- export function preparationMessages(prompt, source) {
69
+ // `aliases` are the narration aliases found in `source`: the model reads the sentences as
70
+ // they will be said. Without any, the request is exactly what it always was. A narration in
71
+ // another `language`: its directions still steer the voice in English, but the model has to
72
+ // know the sentences are not English and must stay as they are. English gets the contract
73
+ // alone, as it always did. A turn of a multi-voice script names its `speaker` too, so the
74
+ // directions suit a narrator, a host or the character saying the line; one voice names none.
75
+ export function preparationMessages(prompt, source, aliases = [], language, speaker) {
76
+ const name = language === undefined || language === "en" ? undefined : languageInfo(language);
68
77
  return [
69
- { role: "system", content: contract },
78
+ {
79
+ role: "system",
80
+ content: name === undefined
81
+ ? contract
82
+ : `${contract}\nThe narration is in ${name.name}; keep it in ${name.name}. Directions stay in English.`,
83
+ },
70
84
  {
71
85
  role: "user",
72
- content: JSON.stringify({ direction: prompt, sentences: sourceSentences(source) }),
86
+ content: JSON.stringify({
87
+ direction: prompt,
88
+ ...(speaker === undefined ? {} : { speaker: `${speaker.name} (${speaker.role})` }),
89
+ sentences: aliasedSentences(sourceSentences(source), aliases),
90
+ }),
73
91
  },
74
92
  ];
75
93
  }
@@ -18,7 +18,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
18
18
  const chunks = chunkNarration(source, chunking).map((text) => {
19
19
  const start = source.indexOf(text, offset);
20
20
  if (start < 0)
21
- throw new Error("Slopify hit an internal error (a narration chunk isn't in the article text). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
21
+ throw new Error("Slopify hit an internal error (a narration chunk isn't in the article text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
22
22
  offset = start + text.length;
23
23
  return { key: nextKey(text), text, start, end: offset };
24
24
  });
@@ -78,7 +78,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
78
78
  }
79
79
  const chunk = chunks[index];
80
80
  if (chunk === undefined)
81
- throw new Error("Slopify hit an internal error (a narration chunk went missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
81
+ throw new Error("Slopify hit an internal error (a narration chunk went missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
82
82
  while ((matches[matchIndex]?.end ?? Infinity) <= chunk.end)
83
83
  matchIndex++;
84
84
  const match = matches[matchIndex];
@@ -90,7 +90,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
90
90
  continue;
91
91
  const first = chunks[from];
92
92
  if (first === undefined)
93
- throw new Error("Slopify hit an internal error (a group of narration chunks is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
93
+ throw new Error("Slopify hit an internal error (a group of narration chunks is empty). Try again; if it happens again, use Download diagnostics in Settings and report it.");
94
94
  const text = source.slice(first.start, chunk.end);
95
95
  result.push(from === index
96
96
  ? { key: chunk.key, text }