@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,206 @@
1
+ import { z } from "zod";
2
+ import { redact } from "../../kernel/log.js";
3
+ import { providerError } from "../../kernel/ports/model.js";
4
+ import { httpFailure, internalError, missingKey, noAudio, providerSaid, refreshList, unreadable, voiceFix, } from "../explain.js";
5
+ import { retryAfter } from "../retry-after.js";
6
+ // The HTTP gateway adapter for Gemini's speech generation (generateContent with an AUDIO
7
+ // response, https://ai.google.dev/gemini-api/docs/speech-generation), billed to a Gemini API
8
+ // key. One voice is `speechConfig.voiceConfig`; a conversation of exactly two voices is
9
+ // `speechConfig.multiSpeakerVoiceConfig`, Gemini's native two-speaker narration. The answer is
10
+ // raw 16-bit mono PCM (24 kHz), which the port cannot carry, so it is encoded to MP3 by the
11
+ // injected encoder before it is handed back.
12
+ export const geminiTtsBase = "https://generativelanguage.googleapis.com/v1beta";
13
+ export const geminiTtsModel = "gemini-2.5-flash-preview-tts";
14
+ // Offline choices only; the picker normally loads the provider catalogue.
15
+ export const geminiTtsModels = [
16
+ { id: "gemini-3.1-flash-tts-preview", name: "Gemini 3.1 Flash TTS (preview)" },
17
+ { id: "gemini-2.5-flash-preview-tts", name: "Gemini 2.5 Flash TTS (preview)" },
18
+ { id: "gemini-2.5-pro-preview-tts", name: "Gemini 2.5 Pro TTS (preview)" },
19
+ ];
20
+ // multiSpeakerVoiceConfig takes exactly two speakers.
21
+ export const geminiDialogueVoices = 2;
22
+ const defaultSampleRate = 24_000;
23
+ const inlineData = z.object({ mimeType: z.string().nullish(), data: z.string().nullish() });
24
+ const answer = z.object({
25
+ candidates: z
26
+ .array(z.object({
27
+ content: z
28
+ .object({ parts: z.array(z.object({ inlineData: inlineData.nullish() })).nullish() })
29
+ .nullish(),
30
+ finishReason: z.string().nullish(),
31
+ }))
32
+ .nullish(),
33
+ promptFeedback: z.object({ blockReason: z.string().nullish() }).nullish(),
34
+ });
35
+ const modelList = z.object({
36
+ models: z
37
+ .array(z.object({
38
+ name: z.string(),
39
+ displayName: z.string().nullish(),
40
+ supportedGenerationMethods: z.array(z.string()).nullish(),
41
+ }))
42
+ .nullish(),
43
+ });
44
+ const errorBody = z.object({
45
+ error: z.object({ message: z.string().nullish(), status: z.string().nullish() }),
46
+ });
47
+ export function geminiTts(deps) {
48
+ return {
49
+ id: "google-tts",
50
+ capabilities: { streams: false, dialogue: true },
51
+ models: async () => {
52
+ const response = await deps.fetch(`${geminiTtsBase}/models?pageSize=1000`, {
53
+ headers: { "x-goog-api-key": keyOf(deps) },
54
+ signal: AbortSignal.timeout(10_000),
55
+ });
56
+ if (!response.ok)
57
+ throw await failure(response, undefined, refreshList);
58
+ const parsed = modelList.safeParse(safeJson(await response.text()));
59
+ if (!parsed.success)
60
+ throw providerError({ kind: "other", message: unreadable("Google") });
61
+ return (parsed.data.models ?? [])
62
+ .filter((model) => /-tts\b|tts-/.test(model.name) &&
63
+ (model.supportedGenerationMethods ?? ["generateContent"]).includes("generateContent"))
64
+ .map((model) => {
65
+ const id = model.name.replace(/^models\//, "");
66
+ return { id, name: model.displayName || id };
67
+ });
68
+ },
69
+ synthesize: async (req) => {
70
+ const model = req.model ?? geminiTtsModel;
71
+ const response = await deps.fetch(`${geminiTtsBase}/models/${encodeURIComponent(model)}:generateContent`, {
72
+ method: "POST",
73
+ signal: req.signal,
74
+ // The key rides Google's own header rather than the query string, which would put it
75
+ // in every proxy log on the way.
76
+ headers: { "x-goog-api-key": keyOf(deps), "Content-Type": "application/json" },
77
+ body: JSON.stringify(geminiSpeechBody(req)),
78
+ });
79
+ if (!response.ok)
80
+ throw await failure(response, req.voiceId);
81
+ const { pcm, sampleRate } = audioOf(await response.text());
82
+ const mp3 = await deps.toMp3(pcm, sampleRate, req.signal);
83
+ return { audio: streamOf(mp3), container: "mp3" };
84
+ },
85
+ };
86
+ }
87
+ // The request body. A dialogue of two voices names each voice as its speaker, since the
88
+ // transcript's speaker names must match `speakerVoiceConfigs`; a dialogue whose lines all share
89
+ // one voice is that voice reading the lines. More than two voices is a grouping bug upstream.
90
+ export function geminiSpeechBody(req) {
91
+ const voices = [...new Set((req.dialogue ?? []).map((line) => line.voiceId))];
92
+ if (voices.length > geminiDialogueVoices)
93
+ throw providerError({
94
+ kind: "unsupported",
95
+ message: internalError(`a Gemini narration request had ${String(voices.length)} voices; Gemini takes at most two`),
96
+ });
97
+ const prebuilt = (voiceName) => ({ prebuiltVoiceConfig: { voiceName } });
98
+ const two = req.dialogue !== undefined && voices.length === geminiDialogueVoices;
99
+ const text = req.dialogue === undefined
100
+ ? req.text
101
+ : two
102
+ ? conversation(req.dialogue, voices)
103
+ : req.dialogue.map((line) => line.text).join("\n");
104
+ return {
105
+ contents: [{ parts: [{ text }] }],
106
+ generationConfig: {
107
+ responseModalities: ["AUDIO"],
108
+ speechConfig: two
109
+ ? {
110
+ multiSpeakerVoiceConfig: {
111
+ speakerVoiceConfigs: voices.map((voice) => ({
112
+ speaker: voice,
113
+ voiceConfig: prebuilt(voice),
114
+ })),
115
+ },
116
+ }
117
+ : { voiceConfig: prebuilt(req.dialogue?.[0]?.voiceId ?? req.voiceId) },
118
+ },
119
+ };
120
+ }
121
+ function conversation(lines, voices) {
122
+ return [
123
+ `TTS the following conversation between ${voices.join(" and ")}:`,
124
+ ...lines.map((line) => `${line.voiceId}: ${line.text.replace(/\s*\n\s*/g, " ")}`),
125
+ ].join("\n");
126
+ }
127
+ function audioOf(text) {
128
+ const parsed = answer.safeParse(safeJson(text));
129
+ if (!parsed.success)
130
+ throw providerError({ kind: "other", message: unreadable("Google") });
131
+ const blocked = parsed.data.promptFeedback?.blockReason;
132
+ if (blocked)
133
+ throw providerError({
134
+ kind: "refusal",
135
+ message: providerSaid("Google", "refused to speak this narration under its content rules", blocked, "Reword that part of the script or article, or choose another voice provider under Speakers (Edit project → Providers)."),
136
+ });
137
+ for (const candidate of parsed.data.candidates ?? [])
138
+ for (const part of candidate.content?.parts ?? []) {
139
+ const data = part.inlineData?.data;
140
+ if (data) {
141
+ const rate = /rate=(\d+)/.exec(part.inlineData?.mimeType ?? "")?.[1];
142
+ return {
143
+ pcm: new Uint8Array(Buffer.from(data, "base64")),
144
+ sampleRate: rate === undefined ? defaultSampleRate : Number(rate),
145
+ };
146
+ }
147
+ }
148
+ throw providerError({ kind: "other", message: noAudio("Google") });
149
+ }
150
+ function streamOf(bytes) {
151
+ return new ReadableStream({
152
+ start(controller) {
153
+ controller.enqueue(bytes);
154
+ controller.close();
155
+ },
156
+ });
157
+ }
158
+ function keyOf(deps) {
159
+ // `missing_key`, not `auth`, because that rule makes it terminal.
160
+ const key = deps.key();
161
+ if (key === undefined || key === "") {
162
+ throw providerError({ kind: "missing_key", message: missingKey("Google") });
163
+ }
164
+ return key;
165
+ }
166
+ function kindOf(status) {
167
+ if (status === 401 || status === 403)
168
+ return "auth";
169
+ if (status === 429)
170
+ return "rate_limit";
171
+ if (status >= 500)
172
+ return "dropped";
173
+ return "other";
174
+ }
175
+ async function failure(response, voiceId, next) {
176
+ const text = await response.text().catch(() => "");
177
+ const parsed = errorBody.safeParse(safeJson(text));
178
+ // Through the redactor: an error body may quote the key back.
179
+ const detail = redact((parsed.success ? parsed.data.error.message : undefined) ??
180
+ (text.trim() || response.statusText));
181
+ // Google answers an unknown key with 400 API_KEY_INVALID rather than 401.
182
+ const badKey = response.status === 400 && text.includes("API_KEY_INVALID");
183
+ const status = badKey ? 401 : response.status;
184
+ const retryAfterMs = retryAfter(response.headers.get("retry-after"));
185
+ return providerError({
186
+ kind: badKey ? "auth" : kindOf(response.status),
187
+ message: httpFailure({
188
+ provider: "Google",
189
+ status,
190
+ detail,
191
+ next,
192
+ ...(voiceId === undefined
193
+ ? {}
194
+ : { subject: `narration request for voice "${voiceId}"`, fix: voiceFix(voiceId) }),
195
+ }),
196
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
197
+ });
198
+ }
199
+ function safeJson(text) {
200
+ try {
201
+ return JSON.parse(text);
202
+ }
203
+ catch {
204
+ return undefined;
205
+ }
206
+ }
@@ -23,7 +23,7 @@ export async function* inworldAsync(deps, request, key) {
23
23
  if (saved === undefined && request.text.length > 100_000) {
24
24
  throw providerError({
25
25
  kind: "unsupported",
26
- message: "This narration part is longer than the 100,000 characters Inworld takes at once (10,000 on On-Demand accounts). Set Chunking to Paragraph in the Providers section of Edit project, then use Retry stage.",
26
+ message: "This narration part is longer than the 100,000 characters Inworld takes at once (10,000 on On-Demand accounts). Set Chunking to Paragraph in the Providers section of Edit project, then use Try again.",
27
27
  });
28
28
  }
29
29
  const headers = { Authorization: `Basic ${key}`, "Content-Type": "application/json" };
@@ -68,7 +68,9 @@ export async function* inworldAsync(deps, request, key) {
68
68
  ? "rate_limit"
69
69
  : code === 3 || code === 5 || code === 9
70
70
  ? "unsupported"
71
- : "other",
71
+ : code === 14
72
+ ? "dropped"
73
+ : "other",
72
74
  message: streamFailure(code, clean(message ?? "", key), request.voiceId),
73
75
  });
74
76
  }
@@ -141,7 +143,9 @@ async function check(response, key, voiceId) {
141
143
  ? "rate_limit"
142
144
  : response.status === 400 || response.status === 404
143
145
  ? "unsupported"
144
- : "other",
146
+ : response.status >= 500
147
+ ? "dropped"
148
+ : "other",
145
149
  message: httpFailure({
146
150
  provider: "Inworld",
147
151
  status: response.status,
@@ -160,7 +164,7 @@ function clean(message, key) {
160
164
  function downloadFailed(status) {
161
165
  return providerError({
162
166
  kind: "other",
163
- message: `Inworld made the narration, but Slopify could not download it${status}. Check your internet connection, then use Retry stage.`,
167
+ message: `Inworld made the narration, but Slopify could not download it${status}. Check your internet connection, then use Try again.`,
164
168
  });
165
169
  }
166
170
  function unreadableAnswer() {
@@ -177,5 +181,5 @@ export function streamFailure(code, detail, voiceId) {
177
181
  return httpFailure({ provider: "Inworld", status: 429, detail, label });
178
182
  if (code === 3 || code === 5 || code === 9)
179
183
  return providerSaid("Inworld", "could not narrate this text", detail, voiceFix(voiceId));
180
- return providerSaid("Inworld", "stopped before the narration was finished", detail, "This is usually temporary: wait a few minutes, then use Retry stage.");
184
+ return providerSaid("Inworld", "stopped before the narration was finished", detail, "This is usually temporary: wait a few minutes, then use Try again.");
181
185
  }
@@ -4,6 +4,7 @@ import { providerError } from "../../kernel/ports/model.js";
4
4
  import { httpFailure, missingKey, noAudio, unreadable, voiceFix } from "../explain.js";
5
5
  import { retryAfter } from "../retry-after.js";
6
6
  import { inworldAsync, streamFailure } from "./inworld-async.js";
7
+ import { inworldLanguages, lookUp } from "./voice-languages.js";
7
8
  export const inworldModels = [
8
9
  { id: "inworld-tts-2", name: "Realtime TTS-2" },
9
10
  { id: "inworld-tts-2-flash", name: "Realtime TTS-2 Flash" },
@@ -16,6 +17,15 @@ export function inworldTts(deps) {
16
17
  return {
17
18
  id: "inworld",
18
19
  capabilities: { streams: true },
20
+ voiceLanguages: async (voiceId, signal) => {
21
+ const key = deps
22
+ .key()
23
+ ?.trim()
24
+ .replace(/^Basic\s+/i, "");
25
+ if (!key)
26
+ return undefined;
27
+ return inworldLanguages(await lookUp(deps.fetch, `https://api.inworld.ai/voices/v1/voices/${encodeURIComponent(voiceId)}`, { Authorization: `Basic ${key}` }, signal));
28
+ },
19
29
  models: async () => inworldModels,
20
30
  synthesize: async (request) => {
21
31
  const key = deps
@@ -59,7 +69,7 @@ async function* synthesizeRequest(deps, request, key) {
59
69
  if (request.text.trim().length === 0)
60
70
  throw providerError({
61
71
  kind: "unsupported",
62
- message: "Inworld was sent an empty piece of narration. Check the article has text in the Article section of Edit project, then use Retry stage.",
72
+ message: "Inworld was sent an empty piece of narration. Check the article has text in the Article section of Edit project, then use Try again.",
63
73
  });
64
74
  if ((request.model ?? "inworld-tts-2") === "inworld-tts-2" && request.text.length > 4000) {
65
75
  yield* inworldAsync(deps, request, key);
@@ -93,7 +103,9 @@ async function* synthesizeRequest(deps, request, key) {
93
103
  ? "rate_limit"
94
104
  : response.status === 400 || response.status === 404
95
105
  ? "unsupported"
96
- : "other",
106
+ : response.status >= 500
107
+ ? "dropped"
108
+ : "other",
97
109
  message: clean(httpFailure({
98
110
  provider: "Inworld",
99
111
  status: response.status,
@@ -128,7 +140,9 @@ async function* synthesizeRequest(deps, request, key) {
128
140
  ? "rate_limit"
129
141
  : code === 3 || code === 5
130
142
  ? "unsupported"
131
- : "other",
143
+ : code === 14
144
+ ? "dropped"
145
+ : "other",
132
146
  message: streamFailure(code, clean(message ?? "", key), request.voiceId),
133
147
  });
134
148
  }
@@ -95,6 +95,9 @@ function kindOf(status) {
95
95
  }
96
96
  // ceiling: as in the OpenRouter adapter, a 400 is retried three more times because the
97
97
  // port's error contract has no terminal kind for a request that cannot be repaired.
98
+ // The provider's own server failed; the same request may well succeed later.
99
+ if (status >= 500)
100
+ return "dropped";
98
101
  return "other";
99
102
  }
100
103
  async function failure(response, voiceId, next) {
@@ -0,0 +1,62 @@
1
+ import { spawn } from "node:child_process";
2
+ import { providerError } from "../../kernel/ports/model.js";
3
+ // Raw 16-bit mono PCM in on stdin, MP3 out on stdout, through the app's own ffmpeg. The bitrate
4
+ // is the one the narration join re-encodes at (`slices/narration/concat.ts`).
5
+ export function pcmToMp3Args(sampleRate) {
6
+ return [
7
+ "-hide_banner",
8
+ "-nostdin",
9
+ "-loglevel",
10
+ "error",
11
+ "-f",
12
+ "s16le",
13
+ "-ar",
14
+ String(sampleRate),
15
+ "-ac",
16
+ "1",
17
+ "-i",
18
+ "pipe:0",
19
+ "-c:a",
20
+ "libmp3lame",
21
+ "-b:a",
22
+ "128k",
23
+ "-f",
24
+ "mp3",
25
+ "pipe:1",
26
+ ];
27
+ }
28
+ export function ffmpegPcmToMp3(bin) {
29
+ return (pcm, sampleRate, signal) => new Promise((resolve, reject) => {
30
+ const child = spawn(bin, pcmToMp3Args(sampleRate), {
31
+ stdio: ["pipe", "pipe", "pipe"],
32
+ windowsHide: true,
33
+ signal,
34
+ });
35
+ const out = [];
36
+ let err = "";
37
+ child.stdout.on("data", (chunk) => out.push(chunk));
38
+ child.stderr.on("data", (chunk) => {
39
+ err = `${err}${chunk.toString("utf8")}`.slice(-2000);
40
+ });
41
+ child.on("error", (error) => {
42
+ reject(signal.aborted
43
+ ? error
44
+ : providerError({
45
+ kind: "other",
46
+ message: `Slopify could not turn Google's audio into MP3 because ffmpeg did not start (${error.message}). Restart Slopify; if it keeps happening, use Download diagnostics in Settings and report it.`,
47
+ }));
48
+ });
49
+ child.on("close", (code) => {
50
+ if (code === 0)
51
+ resolve(new Uint8Array(Buffer.concat(out)));
52
+ else if (!signal.aborted)
53
+ reject(providerError({
54
+ kind: "other",
55
+ message: `Slopify could not turn Google's audio into MP3 (ffmpeg: ${err.trim() || `exit ${String(code)}`}). Use Try again; if it keeps happening, use Download diagnostics in Settings and report it.`,
56
+ }));
57
+ });
58
+ // A closed stdin (ffmpeg gave up early) is reported by the close handler.
59
+ child.stdin.on("error", () => { });
60
+ child.stdin.end(Buffer.from(pcm));
61
+ });
62
+ }
@@ -0,0 +1,147 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { providerError } from "../../kernel/ports/model.js";
4
+ import { defaultSpeechVoice, noSpeechFix, primaryLanguage, } from "../../kernel/ports/system-speech.js";
5
+ export const systemVoiceId = "system-voice";
6
+ // Windows speaks through System.Speech. Every input arrives in an environment variable, so no
7
+ // part of the text or the voice name is ever parsed as PowerShell.
8
+ export const windowsSpeakScript = "Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; try { $s.SelectVoice($env:SLOPIFY_SPEECH_VOICE); $s.SetOutputToWaveFile($env:SLOPIFY_SPEECH_OUT); $s.Speak([System.IO.File]::ReadAllText($env:SLOPIFY_SPEECH_TEXT, [System.Text.Encoding]::UTF8)) } finally { $s.Dispose() }";
9
+ // The program's command for one request. Text goes through a file (or stdin for Piper, an
10
+ // argument for Pico, which takes nothing else), so a line starting with "-" is never a flag.
11
+ export function speakCommand(engine, voice, files, text, env) {
12
+ switch (engine.id) {
13
+ case "say":
14
+ return { binary: "say", args: ["-v", voice.id, "-o", files.out, "-f", files.text] };
15
+ case "sapi":
16
+ return {
17
+ binary: "powershell",
18
+ args: ["-NoProfile", "-NonInteractive", "-Command", windowsSpeakScript],
19
+ env: {
20
+ ...env,
21
+ SLOPIFY_SPEECH_VOICE: voice.id,
22
+ SLOPIFY_SPEECH_OUT: files.out,
23
+ SLOPIFY_SPEECH_TEXT: files.text,
24
+ },
25
+ };
26
+ case "piper":
27
+ return {
28
+ binary: "piper",
29
+ args: ["--model", voice.id, "--output_file", files.out],
30
+ stdin: text,
31
+ };
32
+ case "pico2wave":
33
+ return { binary: "pico2wave", args: ["-l", voice.id, "-w", files.out, "--", text] };
34
+ case "espeak-ng":
35
+ case "espeak":
36
+ return { binary: engine.id, args: ["-v", voice.id, "-w", files.out, "-f", files.text] };
37
+ }
38
+ }
39
+ export function systemTts(deps) {
40
+ const engineFor = async (model) => {
41
+ const found = await deps.detect();
42
+ const engine = model === undefined || model === ""
43
+ ? found.engines[0]
44
+ : found.engines.find((one) => one.id === model);
45
+ if (engine !== undefined)
46
+ return engine;
47
+ throw providerError({
48
+ kind: "unavailable",
49
+ message: found.engines.length === 0
50
+ ? (found.issue ?? `No speech program was found on this computer, ${noSpeechFix}`)
51
+ : `The speech program "${model}" chosen for this narration isn't on this computer any more. In the Edit tab, choose Edit project and pick another System voice model (${found.engines.map((one) => one.name).join(", ")}) under Providers, then use Continue the run.`,
52
+ });
53
+ };
54
+ return {
55
+ id: systemVoiceId,
56
+ capabilities: { streams: false },
57
+ models: async () => (await deps.detect()).engines.map((engine) => ({ id: engine.id, name: engine.name })),
58
+ voiceLanguages: async (voiceId) => {
59
+ try {
60
+ const found = await deps.detect();
61
+ for (const engine of found.engines) {
62
+ const voice = engine.voices.find((one) => one.id === voiceId);
63
+ const language = voice === undefined ? undefined : primaryLanguage(voice);
64
+ if (language !== undefined)
65
+ return [language];
66
+ }
67
+ }
68
+ catch {
69
+ // Never throws: an unknown answer is undefined.
70
+ }
71
+ return undefined;
72
+ },
73
+ synthesize: async (req) => {
74
+ const engine = await engineFor(req.model);
75
+ const voice = engine.voices.find((one) => one.id === req.voiceId) ??
76
+ (req.voiceId === "" || req.voiceId === "default" ? defaultSpeechVoice(engine) : undefined);
77
+ if (voice === undefined)
78
+ throw providerError({
79
+ kind: "unsupported",
80
+ message: `The system voice "${req.voiceId}" isn't installed for ${engine.name} on this computer. Pick one it has in Settings → Voices (Add voice, provider System voice), then choose it for this project and use Continue the run.`,
81
+ });
82
+ if (deps.ffmpeg === undefined)
83
+ throw providerError({
84
+ kind: "unavailable",
85
+ message: "Slopify's ffmpeg isn't ready, so the system voice's recording can't be converted. Restart Slopify and try again.",
86
+ });
87
+ const dir = await mkdtemp(join(deps.tempRoot, "slopify-speech-"));
88
+ try {
89
+ const files = {
90
+ text: join(dir, "text.txt"),
91
+ out: join(dir, engine.id === "say" ? "speech.aiff" : "speech.wav"),
92
+ };
93
+ await writeFile(files.text, req.text, "utf8");
94
+ await runToEnd(deps.run, speakCommand(engine, voice, files, req.text, deps.env), req.signal, engine.name);
95
+ const mp3 = join(dir, "speech.mp3");
96
+ await runToEnd(deps.run, {
97
+ binary: deps.ffmpeg,
98
+ args: [
99
+ "-hide_banner",
100
+ "-loglevel",
101
+ "error",
102
+ "-y",
103
+ "-i",
104
+ files.out,
105
+ "-codec:a",
106
+ "libmp3lame",
107
+ "-q:a",
108
+ "4",
109
+ mp3,
110
+ ],
111
+ }, req.signal, "ffmpeg");
112
+ const bytes = new Uint8Array(await readFile(mp3));
113
+ return { audio: streamOf(bytes), container: "mp3" };
114
+ }
115
+ finally {
116
+ await rm(dir, { recursive: true, force: true });
117
+ }
118
+ },
119
+ };
120
+ }
121
+ async function runToEnd(run, command, signal, label) {
122
+ const child = run(command.binary, command.args, signal, {
123
+ ...(command.stdin === undefined ? {} : { stdin: command.stdin }),
124
+ ...(command.env === undefined ? {} : { env: { ...command.env } }),
125
+ });
126
+ // Drained so a chatty program never blocks on a full pipe.
127
+ for await (const _ of child.stdout) {
128
+ // nothing to keep
129
+ }
130
+ const ended = await child.ended;
131
+ signal.throwIfAborted();
132
+ if (ended.error !== null || ended.code !== 0) {
133
+ const said = child.stderr().trim().split(/\r?\n/).slice(-3).join(" ");
134
+ throw providerError({
135
+ kind: "other",
136
+ message: `The system voice (${label}) couldn't make this narration${said === "" ? (ended.error === null ? "" : `: ${ended.error.message}`) : `: ${said}`}. Try another system voice in Settings → Voices, ${noSpeechFix}`,
137
+ });
138
+ }
139
+ }
140
+ function streamOf(bytes) {
141
+ return new ReadableStream({
142
+ start(controller) {
143
+ controller.enqueue(bytes);
144
+ controller.close();
145
+ },
146
+ });
147
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ // What each provider's voice API says a voice speaks, reduced to lower-case primary language
3
+ // subtags ("es-MX" and "ES_MX" are both "es"). Undefined when the provider said nothing
4
+ // usable: an unknown voice speaks every language as far as the app is concerned.
5
+ export function primaryLanguages(values) {
6
+ const codes = values.flatMap((value) => {
7
+ const code = value?.trim().split(/[-_]/)[0]?.toLowerCase();
8
+ return code !== undefined && /^[a-z]{2,3}$/.test(code) ? [code] : [];
9
+ });
10
+ return codes.length === 0 ? undefined : [...new Set(codes)];
11
+ }
12
+ // Asks once, briefly: the answer only fills a hint in Settings → Voices, so a slow or failed
13
+ // lookup is simply no answer.
14
+ export async function lookUp(fetcher, url, headers, signal) {
15
+ try {
16
+ const response = await fetcher(url, {
17
+ headers,
18
+ signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]),
19
+ });
20
+ if (!response.ok) {
21
+ await response.body?.cancel();
22
+ return undefined;
23
+ }
24
+ return await response.json();
25
+ }
26
+ catch {
27
+ signal.throwIfAborted();
28
+ return undefined;
29
+ }
30
+ }
31
+ // GET /v1/voices/{id}: `verified_languages[].language` (or `.locale`), and the older
32
+ // `labels.language` and `fine_tuning.language`.
33
+ const elevenLabsVoice = z.object({
34
+ verified_languages: z
35
+ .array(z.object({ language: z.string().nullish(), locale: z.string().nullish() }))
36
+ .nullish(),
37
+ labels: z.object({ language: z.string().nullish() }).partial().nullish(),
38
+ fine_tuning: z.object({ language: z.string().nullish() }).partial().nullish(),
39
+ });
40
+ export function elevenLabsLanguages(body) {
41
+ const parsed = elevenLabsVoice.safeParse(body);
42
+ if (!parsed.success)
43
+ return undefined;
44
+ return primaryLanguages([
45
+ ...(parsed.data.verified_languages ?? []).flatMap((one) => [one.language, one.locale]),
46
+ parsed.data.labels?.language,
47
+ parsed.data.fine_tuning?.language,
48
+ ]);
49
+ }
50
+ // GET /voices/{id}: `language` (ISO 639-1) and `accents[].locale`.
51
+ const cartesiaVoice = z.object({
52
+ language: z.string().nullish(),
53
+ accents: z.array(z.object({ locale: z.string().nullish() })).nullish(),
54
+ });
55
+ export function cartesiaLanguages(body) {
56
+ const parsed = cartesiaVoice.safeParse(body);
57
+ if (!parsed.success)
58
+ return undefined;
59
+ return primaryLanguages([
60
+ parsed.data.language,
61
+ ...(parsed.data.accents ?? []).map((one) => one.locale),
62
+ ]);
63
+ }
64
+ // GET /voices/v1/voices/{id}: `languageCode` ("en-US"), `langCode` ("EN_US") and, for a
65
+ // multilingual voice, `promptLanguages`.
66
+ const inworldVoice = z.object({
67
+ languageCode: z.string().nullish(),
68
+ langCode: z.string().nullish(),
69
+ promptLanguages: z.array(z.string()).nullish(),
70
+ });
71
+ export function inworldLanguages(body) {
72
+ const parsed = inworldVoice.safeParse(typeof body === "object" && body !== null && "voice" in body ? body.voice : body);
73
+ if (!parsed.success)
74
+ return undefined;
75
+ return primaryLanguages([
76
+ parsed.data.languageCode,
77
+ parsed.data.langCode,
78
+ ...(parsed.data.promptLanguages ?? []),
79
+ ]);
80
+ }