egaki 0.5.0 → 0.7.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 (280) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/cli/cli.js +1329 -0
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
  22. package/dist/cli/credentials.d.ts.map +1 -0
  23. package/dist/{credentials.js → cli/credentials.js} +88 -5
  24. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  25. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  26. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  27. package/dist/cli/generate.d.ts +140 -0
  28. package/dist/cli/generate.d.ts.map +1 -0
  29. package/dist/cli/generate.js +604 -0
  30. package/dist/cli/generate.test.d.ts +2 -0
  31. package/dist/cli/generate.test.d.ts.map +1 -0
  32. package/dist/cli/generate.test.js +131 -0
  33. package/dist/cli/login.d.ts.map +1 -0
  34. package/dist/{login.js → cli/login.js} +45 -4
  35. package/dist/cli/main.d.ts +3 -0
  36. package/dist/cli/main.d.ts.map +1 -0
  37. package/dist/cli/main.js +7 -0
  38. package/dist/cli/model-catalog.d.ts +141 -0
  39. package/dist/cli/model-catalog.d.ts.map +1 -0
  40. package/dist/cli/model-catalog.js +2135 -0
  41. package/dist/{models.d.ts → cli/models.d.ts} +16 -9
  42. package/dist/cli/models.d.ts.map +1 -0
  43. package/dist/cli/models.js +313 -0
  44. package/dist/cli/open-browser.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.d.ts +3 -0
  46. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  47. package/dist/cli/openai-speech-provider.js +28 -0
  48. package/dist/cli/separation-generate.d.ts +34 -0
  49. package/dist/cli/separation-generate.d.ts.map +1 -0
  50. package/dist/cli/separation-generate.js +117 -0
  51. package/dist/cli/speech-catalog.d.ts +35 -0
  52. package/dist/cli/speech-catalog.d.ts.map +1 -0
  53. package/dist/cli/speech-catalog.js +195 -0
  54. package/dist/cli/speech-generate.d.ts +86 -0
  55. package/dist/cli/speech-generate.d.ts.map +1 -0
  56. package/dist/cli/speech-generate.js +156 -0
  57. package/dist/cli/speech-models.d.ts +8 -0
  58. package/dist/cli/speech-models.d.ts.map +1 -0
  59. package/dist/cli/speech-models.js +109 -0
  60. package/dist/cli/speech-provider.test.d.ts +2 -0
  61. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  62. package/dist/cli/speech-provider.test.js +197 -0
  63. package/dist/cli/subscription.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.d.ts +35 -0
  65. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  66. package/dist/cli/transcription-catalog.js +192 -0
  67. package/dist/cli/transcription-generate.d.ts +62 -0
  68. package/dist/cli/transcription-generate.d.ts.map +1 -0
  69. package/dist/cli/transcription-generate.js +144 -0
  70. package/dist/cli/transcription-models.d.ts +9 -0
  71. package/dist/cli/transcription-models.d.ts.map +1 -0
  72. package/dist/cli/transcription-models.js +100 -0
  73. package/dist/cli/transcription-providers.d.ts +22 -0
  74. package/dist/cli/transcription-providers.d.ts.map +1 -0
  75. package/dist/cli/transcription-providers.js +290 -0
  76. package/dist/cli/video-model-catalog.d.ts +2 -0
  77. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  78. package/dist/cli/video-model-catalog.js +5 -0
  79. package/dist/cli/xai-auth.d.ts +29 -0
  80. package/dist/cli/xai-auth.d.ts.map +1 -0
  81. package/dist/cli/xai-auth.js +325 -0
  82. package/dist/vite/angled-screen.d.ts +57 -0
  83. package/dist/vite/angled-screen.d.ts.map +1 -0
  84. package/dist/vite/angled-screen.js +115 -0
  85. package/dist/vite/app.d.ts +51 -0
  86. package/dist/vite/app.d.ts.map +1 -0
  87. package/dist/vite/app.js +150 -0
  88. package/dist/vite/bands-shader.d.ts +47 -0
  89. package/dist/vite/bands-shader.d.ts.map +1 -0
  90. package/dist/vite/bands-shader.js +267 -0
  91. package/dist/vite/code-block.d.ts +107 -0
  92. package/dist/vite/code-block.d.ts.map +1 -0
  93. package/dist/vite/code-block.js +641 -0
  94. package/dist/vite/components.d.ts +36 -0
  95. package/dist/vite/components.d.ts.map +1 -0
  96. package/dist/vite/components.js +113 -0
  97. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  98. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  99. package/dist/vite/dispersion-rings-shader.js +282 -0
  100. package/dist/vite/easing-curves.d.ts +129 -0
  101. package/dist/vite/easing-curves.d.ts.map +1 -0
  102. package/dist/vite/easing-curves.js +584 -0
  103. package/dist/vite/easing-curves.test.d.ts +2 -0
  104. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  105. package/dist/vite/easing-curves.test.js +200 -0
  106. package/dist/vite/generated-media-client.d.ts +35 -0
  107. package/dist/vite/generated-media-client.d.ts.map +1 -0
  108. package/dist/vite/generated-media-client.js +76 -0
  109. package/dist/vite/keyframes.d.ts +129 -0
  110. package/dist/vite/keyframes.d.ts.map +1 -0
  111. package/dist/vite/keyframes.js +194 -0
  112. package/dist/vite/layout-editor.d.ts +45 -0
  113. package/dist/vite/layout-editor.d.ts.map +1 -0
  114. package/dist/vite/layout-editor.js +587 -0
  115. package/dist/vite/layout-transition.d.ts +136 -0
  116. package/dist/vite/layout-transition.d.ts.map +1 -0
  117. package/dist/vite/layout-transition.js +581 -0
  118. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  119. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  120. package/dist/vite/liquid-gradient-shader.js +413 -0
  121. package/dist/vite/mdx-client.d.ts +15 -0
  122. package/dist/vite/mdx-client.d.ts.map +1 -0
  123. package/dist/vite/mdx-client.js +265 -0
  124. package/dist/vite/mdx-parse.d.ts +102 -0
  125. package/dist/vite/mdx-parse.d.ts.map +1 -0
  126. package/dist/vite/mdx-parse.js +251 -0
  127. package/dist/vite/mdx-provided-components.d.ts +18 -0
  128. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  129. package/dist/vite/mdx-provided-components.js +14 -0
  130. package/dist/vite/mdx-video.d.ts +327 -0
  131. package/dist/vite/mdx-video.d.ts.map +1 -0
  132. package/dist/vite/mdx-video.js +596 -0
  133. package/dist/vite/mdx-video.test.d.ts +9 -0
  134. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  135. package/dist/vite/mdx-video.test.js +1945 -0
  136. package/dist/vite/media-components.d.ts +45 -0
  137. package/dist/vite/media-components.d.ts.map +1 -0
  138. package/dist/vite/media-components.js +355 -0
  139. package/dist/vite/media-duration-store.d.ts +66 -0
  140. package/dist/vite/media-duration-store.d.ts.map +1 -0
  141. package/dist/vite/media-duration-store.js +204 -0
  142. package/dist/vite/player-page.d.ts +35 -0
  143. package/dist/vite/player-page.d.ts.map +1 -0
  144. package/dist/vite/player-page.js +720 -0
  145. package/dist/vite/render-client.d.ts +18 -0
  146. package/dist/vite/render-client.d.ts.map +1 -0
  147. package/dist/vite/render-client.js +67 -0
  148. package/dist/vite/sdk.d.ts +221 -0
  149. package/dist/vite/sdk.d.ts.map +1 -0
  150. package/dist/vite/sdk.js +355 -0
  151. package/dist/vite/server-components.d.ts +55 -0
  152. package/dist/vite/server-components.d.ts.map +1 -0
  153. package/dist/vite/server-components.js +156 -0
  154. package/dist/vite/server-mdx.d.ts +74 -0
  155. package/dist/vite/server-mdx.d.ts.map +1 -0
  156. package/dist/vite/server-mdx.js +259 -0
  157. package/dist/vite/shader-renderer.d.ts +48 -0
  158. package/dist/vite/shader-renderer.d.ts.map +1 -0
  159. package/dist/vite/shader-renderer.js +411 -0
  160. package/dist/vite/store-hooks.d.ts +12 -0
  161. package/dist/vite/store-hooks.d.ts.map +1 -0
  162. package/dist/vite/store-hooks.js +93 -0
  163. package/dist/vite/store.d.ts +75 -0
  164. package/dist/vite/store.d.ts.map +1 -0
  165. package/dist/vite/store.js +52 -0
  166. package/dist/vite/store.test.d.ts +6 -0
  167. package/dist/vite/store.test.d.ts.map +1 -0
  168. package/dist/vite/store.test.js +172 -0
  169. package/dist/vite/tweakpane-hook.d.ts +94 -0
  170. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  171. package/dist/vite/tweakpane-hook.js +450 -0
  172. package/dist/vite/vite-plugin.d.ts +28 -0
  173. package/dist/vite/vite-plugin.d.ts.map +1 -0
  174. package/dist/vite/vite-plugin.js +538 -0
  175. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  176. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  177. package/dist/vite/wave-gradient-shader.js +207 -0
  178. package/package.json +103 -25
  179. package/skills/egaki/SKILL.md +162 -1
  180. package/src/cli/cache-utils.ts +169 -0
  181. package/src/cli/cached-generate.test.ts +531 -0
  182. package/src/cli/cached-generate.ts +348 -0
  183. package/src/cli/cartesia-provider.ts +249 -0
  184. package/src/cli/cli.ts +1818 -0
  185. package/src/cli/clone-generate.ts +202 -0
  186. package/src/{credentials.ts → cli/credentials.ts} +90 -7
  187. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  188. package/src/cli/generate.test.ts +166 -0
  189. package/src/cli/generate.ts +912 -0
  190. package/src/{login.ts → cli/login.ts} +48 -3
  191. package/src/cli/main.ts +8 -0
  192. package/src/cli/model-catalog.ts +2336 -0
  193. package/src/cli/models.ts +408 -0
  194. package/src/cli/openai-speech-provider.ts +34 -0
  195. package/src/cli/separation-generate.ts +187 -0
  196. package/src/cli/speech-catalog.ts +253 -0
  197. package/src/cli/speech-generate.ts +275 -0
  198. package/src/cli/speech-models.ts +146 -0
  199. package/src/cli/speech-provider.test.ts +226 -0
  200. package/src/cli/transcription-catalog.ts +250 -0
  201. package/src/cli/transcription-generate.ts +214 -0
  202. package/src/cli/transcription-models.ts +136 -0
  203. package/src/cli/transcription-providers.ts +427 -0
  204. package/src/cli/video-model-catalog.ts +14 -0
  205. package/src/cli/xai-auth.ts +396 -0
  206. package/src/vite/angled-screen.tsx +214 -0
  207. package/src/vite/app.tsx +197 -0
  208. package/src/vite/bands-shader.tsx +319 -0
  209. package/src/vite/code-block.tsx +939 -0
  210. package/src/vite/components.tsx +278 -0
  211. package/src/vite/dispersion-rings-shader.tsx +331 -0
  212. package/src/vite/easing-curves.test.ts +232 -0
  213. package/src/vite/easing-curves.ts +743 -0
  214. package/src/vite/generated-media-client.tsx +121 -0
  215. package/src/vite/keyframes.tsx +347 -0
  216. package/src/vite/layout-editor.tsx +709 -0
  217. package/src/vite/layout-transition.tsx +706 -0
  218. package/src/vite/liquid-gradient-shader.tsx +474 -0
  219. package/src/vite/mdx-client.tsx +420 -0
  220. package/src/vite/mdx-parse.ts +343 -0
  221. package/src/vite/mdx-provided-components.ts +19 -0
  222. package/src/vite/mdx-video.test.tsx +2219 -0
  223. package/src/vite/mdx-video.tsx +976 -0
  224. package/src/vite/media-components.tsx +473 -0
  225. package/src/vite/media-duration-store.ts +225 -0
  226. package/src/vite/motion-timing.ts +194 -0
  227. package/src/vite/player-page.tsx +1107 -0
  228. package/src/vite/render-client.ts +84 -0
  229. package/src/vite/sdk.ts +598 -0
  230. package/src/vite/server-components.tsx +241 -0
  231. package/src/vite/server-mdx.ts +285 -0
  232. package/src/vite/shader-renderer.tsx +535 -0
  233. package/src/vite/store-hooks.ts +117 -0
  234. package/src/vite/store.test.ts +202 -0
  235. package/src/vite/store.ts +115 -0
  236. package/src/vite/styles.css +21 -0
  237. package/src/vite/tweakpane-hook.tsx +593 -0
  238. package/src/vite/virtual-modules.d.ts +30 -0
  239. package/src/vite/vite-plugin.ts +584 -0
  240. package/src/vite/wave-gradient-shader.tsx +253 -0
  241. package/README.md +0 -266
  242. package/dist/antigravity-auth.d.ts +0 -15
  243. package/dist/antigravity-auth.d.ts.map +0 -1
  244. package/dist/antigravity-auth.js +0 -544
  245. package/dist/antigravity-image.d.ts +0 -11
  246. package/dist/antigravity-image.d.ts.map +0 -1
  247. package/dist/antigravity-image.js +0 -285
  248. package/dist/chatgpt-auth.d.ts.map +0 -1
  249. package/dist/cli.d.ts +0 -3
  250. package/dist/cli.d.ts.map +0 -1
  251. package/dist/cli.js +0 -968
  252. package/dist/credentials.d.ts.map +0 -1
  253. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  254. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  255. package/dist/gateway-model-candidates.generated.js +0 -64
  256. package/dist/login.d.ts.map +0 -1
  257. package/dist/model-catalog.d.ts +0 -42
  258. package/dist/model-catalog.d.ts.map +0 -1
  259. package/dist/model-catalog.js +0 -1001
  260. package/dist/models.d.ts.map +0 -1
  261. package/dist/models.js +0 -246
  262. package/dist/open-browser.d.ts.map +0 -1
  263. package/dist/subscription.d.ts.map +0 -1
  264. package/dist/video-model-catalog.d.ts +0 -52
  265. package/dist/video-model-catalog.d.ts.map +0 -1
  266. package/dist/video-model-catalog.js +0 -306
  267. package/src/cli.ts +0 -1378
  268. package/src/model-catalog.ts +0 -1077
  269. package/src/models.ts +0 -309
  270. package/src/video-model-catalog.ts +0 -363
  271. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  272. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  273. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  274. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  275. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  276. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  277. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  278. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  279. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  280. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
@@ -0,0 +1,26 @@
1
+ export type CloneProvider = 'cartesia' | 'elevenlabs';
2
+ export interface CloneVoiceOptions {
3
+ /** Raw audio bytes of the clip to clone from. */
4
+ audio: Uint8Array;
5
+ /** Name for the cloned voice. */
6
+ name: string;
7
+ /** Which provider to use. */
8
+ provider: CloneProvider;
9
+ /** Cartesia only: ISO 639-1 language code. Default: 'en'. Ignored by ElevenLabs. */
10
+ language?: string;
11
+ /** Optional description for the voice. */
12
+ description?: string;
13
+ /** Original filename for MIME type inference. */
14
+ filename?: string;
15
+ /** Cartesia: optional base voice ID to derive from. */
16
+ baseVoiceId?: string;
17
+ /** ElevenLabs: apply AI noise removal to the clip. */
18
+ removeBackgroundNoise?: boolean;
19
+ }
20
+ export interface CloneVoiceResult {
21
+ voiceId: string;
22
+ name: string;
23
+ provider: CloneProvider;
24
+ }
25
+ export declare function cloneVoiceUncached(opts: CloneVoiceOptions): Promise<Error | CloneVoiceResult>;
26
+ //# sourceMappingURL=clone-generate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clone-generate.d.ts","sourceRoot":"","sources":["../../src/cli/clone-generate.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,YAAY,CAAA;AAErD,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,KAAK,EAAE,UAAU,CAAA;IACjB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,6BAA6B;IAC7B,QAAQ,EAAE,aAAa,CAAA;IACvB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sDAAsD;IACtD,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,aAAa,CAAA;CACxB;AAoJD,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAUnC"}
@@ -0,0 +1,148 @@
1
+ // Voice cloning via Cartesia and ElevenLabs APIs.
2
+ // Both are simple multipart/form-data HTTP calls, no SDK needed.
3
+ // Returns Error | Result (errore style) instead of throwing.
4
+ //
5
+ // Cartesia: POST https://api.cartesia.ai/voices/clone
6
+ // Instant clone, up to 10s of audio, free.
7
+ // Docs: https://docs.cartesia.ai/api-reference/voices/clone
8
+ // Best practices: https://docs.cartesia.ai/build-with-cartesia/capability-guides/clone-voices
9
+ //
10
+ // ElevenLabs: POST https://api.elevenlabs.io/v1/voices/add
11
+ // Instant voice clone (IVC), 1-3 min recommended.
12
+ // Docs: https://elevenlabs.io/docs/api-reference/voices/ivc/create
13
+ import { injectCredentialsToEnv } from './credentials.js';
14
+ // ─── validation ─────────────────────────────────────────────────────────────
15
+ const CARTESIA_LANGUAGES = new Set([
16
+ 'en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl',
17
+ 'ru', 'sv', 'tr', 'tl', 'bg', 'ro', 'ar', 'cs', 'el', 'fi', 'hr', 'ms',
18
+ 'sk', 'da', 'ta', 'uk', 'hu', 'no', 'vi', 'bn', 'th', 'he', 'ka', 'id',
19
+ 'te', 'gu', 'kn', 'ml', 'mr', 'pa',
20
+ ]);
21
+ function validateOptions(opts) {
22
+ if (opts.audio.length === 0) {
23
+ return new Error('No audio data provided. Pass an audio file or use --stdin.');
24
+ }
25
+ if (!opts.name || opts.name.trim().length === 0) {
26
+ return new Error('Voice name is required. Use --name "My Voice".');
27
+ }
28
+ if (opts.provider === 'cartesia' && opts.language && !CARTESIA_LANGUAGES.has(opts.language)) {
29
+ return new Error(`Unsupported language "${opts.language}" for Cartesia. ` +
30
+ `Supported: ${[...CARTESIA_LANGUAGES].join(', ')}`);
31
+ }
32
+ }
33
+ // ─── MIME type helper ───────────────────────────────────────────────────────
34
+ function mimeTypeFromFilename(filename) {
35
+ if (!filename)
36
+ return 'audio/mpeg';
37
+ const ext = filename.split('.').pop()?.toLowerCase();
38
+ const map = {
39
+ mp3: 'audio/mpeg',
40
+ wav: 'audio/wav',
41
+ flac: 'audio/flac',
42
+ ogg: 'audio/ogg',
43
+ oga: 'audio/ogg',
44
+ webm: 'audio/webm',
45
+ mpga: 'audio/mpeg',
46
+ m4a: 'audio/mp4',
47
+ };
48
+ return map[ext ?? ''] ?? 'audio/mpeg';
49
+ }
50
+ // ─── Cartesia clone ─────────────────────────────────────────────────────────
51
+ const CARTESIA_API_BASE = 'https://api.cartesia.ai';
52
+ const CARTESIA_API_VERSION = '2026-03-01';
53
+ async function cloneVoiceCartesia(opts) {
54
+ const apiKey = process.env['CARTESIA_API_KEY'];
55
+ if (!apiKey) {
56
+ return new Error('CARTESIA_API_KEY not found. Run: egaki login --provider cartesia\n' +
57
+ 'Get your key at https://play.cartesia.ai/keys');
58
+ }
59
+ const language = opts.language ?? 'en';
60
+ const mime = mimeTypeFromFilename(opts.filename);
61
+ const ext = opts.filename?.split('.').pop() ?? 'mp3';
62
+ const formData = new FormData();
63
+ const blob = new Blob([Buffer.from(opts.audio)], { type: mime });
64
+ formData.append('clip', new File([blob], `clip.${ext}`, { type: mime }));
65
+ formData.append('name', opts.name.trim());
66
+ formData.append('language', language);
67
+ if (opts.description)
68
+ formData.append('description', opts.description);
69
+ if (opts.baseVoiceId)
70
+ formData.append('base_voice_id', opts.baseVoiceId);
71
+ const response = await fetch(`${CARTESIA_API_BASE}/voices/clone`, {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Cartesia-Version': CARTESIA_API_VERSION,
75
+ 'Authorization': `Bearer ${apiKey}`,
76
+ },
77
+ body: formData,
78
+ }).catch((cause) => new Error('Cartesia clone request failed', { cause }));
79
+ if (response instanceof Error)
80
+ return response;
81
+ if (!response.ok) {
82
+ const errorText = await response.text().catch(() => '');
83
+ return new Error(`Cartesia clone API error ${response.status}: ${errorText || response.statusText}`);
84
+ }
85
+ const json = await response.json().catch((cause) => new Error('Cartesia clone API returned invalid JSON', { cause }));
86
+ if (json instanceof Error)
87
+ return json;
88
+ if (!json.id)
89
+ return new Error('Cartesia clone API response did not include a voice id');
90
+ return {
91
+ voiceId: json.id,
92
+ name: json.name ?? opts.name.trim(),
93
+ provider: 'cartesia',
94
+ };
95
+ }
96
+ // ─── ElevenLabs clone ───────────────────────────────────────────────────────
97
+ const ELEVENLABS_API_BASE = 'https://api.elevenlabs.io';
98
+ async function cloneVoiceElevenLabs(opts) {
99
+ const apiKey = process.env['ELEVENLABS_API_KEY'];
100
+ if (!apiKey) {
101
+ return new Error('ELEVENLABS_API_KEY not found. Run: egaki login --provider elevenlabs\n' +
102
+ 'Get your key at https://elevenlabs.io/app/settings/api-keys');
103
+ }
104
+ const mime = mimeTypeFromFilename(opts.filename);
105
+ const ext = opts.filename?.split('.').pop() ?? 'mp3';
106
+ const formData = new FormData();
107
+ formData.append('name', opts.name.trim());
108
+ const blob = new Blob([Buffer.from(opts.audio)], { type: mime });
109
+ formData.append('files', new File([blob], `clip.${ext}`, { type: mime }));
110
+ if (opts.description)
111
+ formData.append('description', opts.description);
112
+ if (opts.removeBackgroundNoise)
113
+ formData.append('remove_background_noise', 'true');
114
+ const response = await fetch(`${ELEVENLABS_API_BASE}/v1/voices/add`, {
115
+ method: 'POST',
116
+ headers: {
117
+ 'xi-api-key': apiKey,
118
+ },
119
+ body: formData,
120
+ }).catch((cause) => new Error('ElevenLabs clone request failed', { cause }));
121
+ if (response instanceof Error)
122
+ return response;
123
+ if (!response.ok) {
124
+ const errorText = await response.text().catch(() => '');
125
+ return new Error(`ElevenLabs clone API error ${response.status}: ${errorText || response.statusText}`);
126
+ }
127
+ const json = await response.json().catch((cause) => new Error('ElevenLabs clone API returned invalid JSON', { cause }));
128
+ if (json instanceof Error)
129
+ return json;
130
+ if (!json.voice_id)
131
+ return new Error('ElevenLabs clone API response did not include a voice_id');
132
+ return {
133
+ voiceId: json.voice_id,
134
+ name: opts.name.trim(),
135
+ provider: 'elevenlabs',
136
+ };
137
+ }
138
+ // ─── main entry point ───────────────────────────────────────────────────────
139
+ export async function cloneVoiceUncached(opts) {
140
+ injectCredentialsToEnv();
141
+ const validationError = validateOptions(opts);
142
+ if (validationError)
143
+ return validationError;
144
+ if (opts.provider === 'elevenlabs') {
145
+ return cloneVoiceElevenLabs(opts);
146
+ }
147
+ return cloneVoiceCartesia(opts);
148
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ChatGptAuth } from './chatgpt-auth.js';
2
+ import type { XaiAuth } from './xai-auth.js';
2
3
  export type ProviderInfo = {
3
4
  envVar: string;
4
5
  label: string;
@@ -8,14 +9,16 @@ export type ProviderInfo = {
8
9
  };
9
10
  export declare const PROVIDERS: Record<string, ProviderInfo>;
10
11
  export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
11
- /** A credential value is either a plain API key string or a ChatGPT OAuth object. */
12
- export type CredentialValue = string | ChatGptAuth;
12
+ /** A credential value is a plain API key string, a ChatGPT OAuth object, or an xAI OAuth object. */
13
+ export type CredentialValue = string | ChatGptAuth | XaiAuth;
13
14
  export type Credentials = Record<string, CredentialValue>;
14
15
  export declare function readCredentials(): Credentials;
15
16
  export declare function writeCredentials(creds: Credentials): void;
16
17
  export declare function saveProviderKey(provider: string, key: string): void;
17
18
  export declare function saveChatGptAuth(auth: ChatGptAuth): void;
18
19
  export declare function getChatGptAuth(): ChatGptAuth | undefined;
20
+ export declare function saveXaiAuth(auth: XaiAuth): void;
21
+ export declare function getXaiAuth(): XaiAuth | undefined;
19
22
  export declare function removeProviderKey(provider: string): void;
20
23
  export declare function getProviderKey(provider: string): string | undefined;
21
24
  export declare function injectCredentialsToEnv(): void;
@@ -24,6 +27,12 @@ export declare function injectCredentialsToEnv(): void;
24
27
  * for OpenAI image models instead of a direct OpenAI API key.
25
28
  */
26
29
  export declare function shouldUseChatGptBackend(): boolean;
30
+ /**
31
+ * Returns true when xAI models should use the OAuth flow instead of a direct
32
+ * XAI_API_KEY. Priority: explicit env var > xai-oauth tokens > stored xai key.
33
+ * Only an explicit XAI_API_KEY (set before egaki runs) overrides OAuth.
34
+ */
35
+ export declare function shouldUseXaiOAuth(): boolean;
27
36
  export declare function getKeyStatus(provider: string): {
28
37
  available: boolean;
29
38
  source: 'env' | 'stored' | 'oauth' | 'none';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/cli/credentials.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAE5C,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAKD,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAoElD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,oGAAoG;AACpG,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAA;AAC5D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAEzD,wBAAgB,eAAe,IAAI,WAAW,CAO7C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIvD;AAED,wBAAgB,cAAc,IAAI,WAAW,GAAG,SAAS,CAOxD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAI/C;AAED,wBAAgB,UAAU,IAAI,OAAO,GAAG,SAAS,CAKhD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAInE;AASD,wBAAgB,sBAAsB,IAAI,IAAI,CAkB7C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAG3C;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;CAC5C,CA0CA"}
@@ -2,8 +2,8 @@
2
2
  // Reads and writes API keys from ~/.config/egaki/credentials.json.
3
3
  // Keys are stored per-provider and injected as env vars at runtime.
4
4
  //
5
- // Most providers store a plain API key string. The 'chatgpt' provider stores
6
- // a structured ChatGptAuth object with OAuth tokens for the Codex backend flow.
5
+ // Most providers store a plain API key string. OAuth providers store structured
6
+ // token objects for their custom backend flows.
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import os from 'node:os';
@@ -47,6 +47,37 @@ export const PROVIDERS = {
47
47
  label: 'fal.ai (Flux, SDXL, Recraft)',
48
48
  hint: 'Get your key at https://fal.ai/dashboard/keys',
49
49
  },
50
+ xai: {
51
+ envVar: 'XAI_API_KEY',
52
+ label: 'xAI (API key)',
53
+ hint: 'Get your key at https://console.x.ai/team/default/api-keys',
54
+ },
55
+ 'xai-oauth': {
56
+ envVar: 'XAI_API_KEY',
57
+ label: 'xAI Grok Build (use your subscription)',
58
+ hint: 'Sign in with your xAI / Grok account via browser',
59
+ oauth: true,
60
+ },
61
+ elevenlabs: {
62
+ envVar: 'ELEVENLABS_API_KEY',
63
+ label: 'ElevenLabs (speech)',
64
+ hint: 'Get your key at https://elevenlabs.io/app/settings/api-keys',
65
+ },
66
+ cartesia: {
67
+ envVar: 'CARTESIA_API_KEY',
68
+ label: 'Cartesia (speech)',
69
+ hint: 'Get your key at https://play.cartesia.ai/keys',
70
+ },
71
+ deepgram: {
72
+ envVar: 'DEEPGRAM_API_KEY',
73
+ label: 'Deepgram (transcription)',
74
+ hint: 'Get your key at https://console.deepgram.com/api-keys',
75
+ },
76
+ groq: {
77
+ envVar: 'GROQ_API_KEY',
78
+ label: 'Groq (fast transcription)',
79
+ hint: 'Get your key at https://console.groq.com/keys',
80
+ },
50
81
  };
51
82
  export const EGAKI_GATEWAY_URL = 'https://egaki.org/v3/ai';
52
83
  function getConfigDir() {
@@ -92,6 +123,18 @@ export function getChatGptAuth() {
92
123
  }
93
124
  return undefined;
94
125
  }
126
+ export function saveXaiAuth(auth) {
127
+ const creds = readCredentials();
128
+ creds['xai-oauth'] = auth;
129
+ writeCredentials(creds);
130
+ }
131
+ export function getXaiAuth() {
132
+ const creds = readCredentials();
133
+ const val = creds['xai-oauth'];
134
+ if (!val || typeof val !== 'object')
135
+ return undefined;
136
+ return val;
137
+ }
95
138
  export function removeProviderKey(provider) {
96
139
  const creds = readCredentials();
97
140
  delete creds[provider];
@@ -102,18 +145,28 @@ export function getProviderKey(provider) {
102
145
  const val = creds[provider];
103
146
  return typeof val === 'string' ? val : undefined;
104
147
  }
105
- // Inject all stored non-ChatGPT credentials as env vars so the AI SDK
148
+ // Inject all stored non-OAuth credentials as env vars so the AI SDK
106
149
  // providers pick them up automatically. Env vars already set by the user take
107
150
  // precedence (we don't overwrite them).
151
+ //
152
+ // Special case: if xAI OAuth tokens exist, skip injecting the stored xai API
153
+ // key so OAuth takes priority. Only an explicit XAI_API_KEY env var (set by
154
+ // the user before egaki runs) will override OAuth.
108
155
  export function injectCredentialsToEnv() {
109
156
  const creds = readCredentials();
157
+ const hasXaiOAuth = Boolean(getXaiAuth());
110
158
  for (const [provider, value] of Object.entries(creds)) {
111
159
  const info = PROVIDERS[provider];
112
160
  if (!info)
113
161
  continue;
114
- // Skip ChatGPT OAuth entries — handled separately
162
+ // Skip OAuth entries — handled separately via custom fetch
115
163
  if (provider === 'chatgpt')
116
164
  continue;
165
+ if (provider === 'xai-oauth')
166
+ continue;
167
+ // Skip stored xai key when OAuth tokens exist (OAuth takes priority)
168
+ if (provider === 'xai' && hasXaiOAuth && !process.env[info.envVar])
169
+ continue;
117
170
  if (typeof value === 'string' && !process.env[info.envVar]) {
118
171
  process.env[info.envVar] = value;
119
172
  }
@@ -126,6 +179,16 @@ export function injectCredentialsToEnv() {
126
179
  export function shouldUseChatGptBackend() {
127
180
  return !process.env['OPENAI_API_KEY'] && Boolean(getChatGptAuth());
128
181
  }
182
+ /**
183
+ * Returns true when xAI models should use the OAuth flow instead of a direct
184
+ * XAI_API_KEY. Priority: explicit env var > xai-oauth tokens > stored xai key.
185
+ * Only an explicit XAI_API_KEY (set before egaki runs) overrides OAuth.
186
+ */
187
+ export function shouldUseXaiOAuth() {
188
+ if (process.env['XAI_API_KEY'])
189
+ return false;
190
+ return Boolean(getXaiAuth());
191
+ }
129
192
  // Check if a provider has a key available (from env or stored credentials).
130
193
  // Returns the source of the key for display purposes.
131
194
  export function getKeyStatus(provider) {
@@ -133,7 +196,7 @@ export function getKeyStatus(provider) {
133
196
  if (!info) {
134
197
  return { available: false, source: 'none' };
135
198
  }
136
- // ChatGPT OAuth is a special case
199
+ // OAuth providers are special cases
137
200
  if (provider === 'chatgpt') {
138
201
  const auth = getChatGptAuth();
139
202
  if (auth) {
@@ -141,6 +204,26 @@ export function getKeyStatus(provider) {
141
204
  }
142
205
  return { available: false, source: 'none' };
143
206
  }
207
+ if (provider === 'xai-oauth') {
208
+ const auth = getXaiAuth();
209
+ if (auth) {
210
+ return { available: true, source: 'oauth' };
211
+ }
212
+ return { available: false, source: 'none' };
213
+ }
214
+ // For xai provider, report the same priority used by generation:
215
+ // explicit env var > xai-oauth tokens > stored xai key.
216
+ if (provider === 'xai') {
217
+ if (process.env[info.envVar])
218
+ return { available: true, source: 'env' };
219
+ const oauthAuth = getXaiAuth();
220
+ if (oauthAuth)
221
+ return { available: true, source: 'oauth' };
222
+ const stored = getProviderKey(provider);
223
+ if (stored)
224
+ return { available: true, source: 'stored' };
225
+ return { available: false, source: 'none' };
226
+ }
144
227
  if (process.env[info.envVar]) {
145
228
  return { available: true, source: 'env' };
146
229
  }
@@ -0,0 +1,26 @@
1
+ import type { SpeechProvider } from './speech-generate.js';
2
+ import type { WordTimestamp } from './transcription-generate.js';
3
+ interface CharacterAlignment {
4
+ characters: string[];
5
+ character_start_times_seconds: number[];
6
+ character_end_times_seconds: number[];
7
+ }
8
+ /**
9
+ * Group character-level timestamps into word-level timestamps.
10
+ * Characters are grouped by splitting on whitespace characters.
11
+ * Each word's start time is the start of its first character,
12
+ * and its end time is the end of its last character.
13
+ */
14
+ export declare function groupCharactersToWords(alignment: CharacterAlignment): WordTimestamp[];
15
+ /**
16
+ * Parse an ElevenLabs NDJSON stream into audio chunks and character alignment.
17
+ * Each line is a JSON object with audio_base64 and optional alignment data.
18
+ * Alignment is typically only in the first chunk.
19
+ */
20
+ export declare function parseElevenLabsStream(body: ReadableStream<Uint8Array>): Promise<{
21
+ audioChunks: Uint8Array[];
22
+ alignment?: CharacterAlignment;
23
+ }>;
24
+ export declare function createElevenLabsSpeechProvider(): SpeechProvider;
25
+ export {};
26
+ //# sourceMappingURL=elevenlabs-speech-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevenlabs-speech-provider.d.ts","sourceRoot":"","sources":["../../src/cli/elevenlabs-speech-provider.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAA+C,MAAM,sBAAsB,CAAA;AACvG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAsChE,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,6BAA6B,EAAE,MAAM,EAAE,CAAA;IACvC,2BAA2B,EAAE,MAAM,EAAE,CAAA;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,kBAAkB,GAAG,aAAa,EAAE,CA+BrF;AAYD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC;IAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAsDxE;AAgBD,wBAAgB,8BAA8B,IAAI,cAAc,CA2D/D"}
@@ -0,0 +1,199 @@
1
+ // ElevenLabs speech provider for egaki.
2
+ // Uses the streaming endpoint /v1/text-to-speech/{voiceId}/stream/with-timestamps
3
+ // to get audio chunks + character timestamps in a single streamed response.
4
+ // Character-level alignment is grouped into word-level timestamps.
5
+ //
6
+ // API docs: https://elevenlabs.io/docs/api-reference/text-to-speech/stream-with-timestamps
7
+ //
8
+ // The streaming response is NDJSON (newline-delimited JSON), NOT SSE.
9
+ // Each line is a JSON object: { audio_base64, alignment?, normalized_alignment? }
10
+ // Alignment data (character-level timing) is included in the first chunk only.
11
+ // We use `normalized_alignment` (timing of what was actually spoken).
12
+ const ELEVENLABS_API_BASE = 'https://api.elevenlabs.io';
13
+ const DEFAULT_VOICE_ID = '21m00Tcm4TlvDq8ikWAM'; // Rachel
14
+ // ─── format mapping ──────────────────────────────────────────────────────────
15
+ const FORMAT_MAP = {
16
+ mp3: 'mp3_44100_128',
17
+ mp3_32: 'mp3_44100_32',
18
+ mp3_64: 'mp3_44100_64',
19
+ mp3_96: 'mp3_44100_96',
20
+ mp3_128: 'mp3_44100_128',
21
+ mp3_192: 'mp3_44100_192',
22
+ pcm: 'pcm_44100',
23
+ pcm_16000: 'pcm_16000',
24
+ pcm_22050: 'pcm_22050',
25
+ pcm_24000: 'pcm_24000',
26
+ pcm_44100: 'pcm_44100',
27
+ ulaw: 'ulaw_8000',
28
+ };
29
+ function resolveOutputFormat(format) {
30
+ if (!format)
31
+ return 'mp3_44100_128';
32
+ return FORMAT_MAP[format] || format;
33
+ }
34
+ function formatToMediaType(format) {
35
+ if (format.startsWith('mp3'))
36
+ return 'audio/mpeg';
37
+ if (format.startsWith('pcm'))
38
+ return 'audio/pcm';
39
+ if (format.startsWith('ulaw'))
40
+ return 'audio/basic';
41
+ if (format.startsWith('opus'))
42
+ return 'audio/opus';
43
+ if (format.startsWith('wav'))
44
+ return 'audio/wav';
45
+ return 'audio/mpeg';
46
+ }
47
+ /**
48
+ * Group character-level timestamps into word-level timestamps.
49
+ * Characters are grouped by splitting on whitespace characters.
50
+ * Each word's start time is the start of its first character,
51
+ * and its end time is the end of its last character.
52
+ */
53
+ export function groupCharactersToWords(alignment) {
54
+ const words = [];
55
+ let currentWord = '';
56
+ let wordStart = -1;
57
+ let wordEnd = -1;
58
+ for (let i = 0; i < alignment.characters.length; i++) {
59
+ const char = alignment.characters[i];
60
+ const charStart = alignment.character_start_times_seconds[i];
61
+ const charEnd = alignment.character_end_times_seconds[i];
62
+ if (/\s/.test(char)) {
63
+ if (currentWord) {
64
+ words.push({ word: currentWord, startSecond: wordStart, endSecond: wordEnd });
65
+ currentWord = '';
66
+ wordStart = -1;
67
+ wordEnd = -1;
68
+ }
69
+ continue;
70
+ }
71
+ currentWord += char;
72
+ if (wordStart < 0)
73
+ wordStart = charStart;
74
+ wordEnd = charEnd;
75
+ }
76
+ if (currentWord) {
77
+ words.push({ word: currentWord, startSecond: wordStart, endSecond: wordEnd });
78
+ }
79
+ return words;
80
+ }
81
+ // ─── NDJSON stream parser ────────────────────────────────────────────────────
82
+ /**
83
+ * Parse an ElevenLabs NDJSON stream into audio chunks and character alignment.
84
+ * Each line is a JSON object with audio_base64 and optional alignment data.
85
+ * Alignment is typically only in the first chunk.
86
+ */
87
+ export async function parseElevenLabsStream(body) {
88
+ const audioChunks = [];
89
+ let alignment;
90
+ const reader = body.pipeThrough(new TextDecoderStream()).getReader();
91
+ let buffer = '';
92
+ while (true) {
93
+ const { done, value } = await reader.read();
94
+ if (done)
95
+ break;
96
+ buffer += value;
97
+ // Process complete lines
98
+ let newlineIdx;
99
+ while ((newlineIdx = buffer.indexOf('\n')) >= 0) {
100
+ const line = buffer.slice(0, newlineIdx).trim();
101
+ buffer = buffer.slice(newlineIdx + 1);
102
+ if (!line)
103
+ continue;
104
+ let chunk;
105
+ try {
106
+ chunk = JSON.parse(line);
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ if (chunk.audio_base64) {
112
+ audioChunks.push(new Uint8Array(Buffer.from(chunk.audio_base64, 'base64')));
113
+ }
114
+ // Capture alignment from first chunk that has it
115
+ if (!alignment) {
116
+ const align = chunk.normalized_alignment || chunk.alignment;
117
+ if (align)
118
+ alignment = align;
119
+ }
120
+ }
121
+ }
122
+ // Process any remaining data in buffer (last line without trailing newline)
123
+ const remaining = buffer.trim();
124
+ if (remaining) {
125
+ try {
126
+ const chunk = JSON.parse(remaining);
127
+ if (chunk.audio_base64) {
128
+ audioChunks.push(new Uint8Array(Buffer.from(chunk.audio_base64, 'base64')));
129
+ }
130
+ if (!alignment) {
131
+ const align = chunk.normalized_alignment || chunk.alignment;
132
+ if (align)
133
+ alignment = align;
134
+ }
135
+ }
136
+ catch { /* ignore malformed trailing data */ }
137
+ }
138
+ return { audioChunks, alignment };
139
+ }
140
+ /** Concatenate multiple Uint8Array chunks into one. */
141
+ function concatUint8Arrays(chunks) {
142
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
143
+ const result = new Uint8Array(totalLength);
144
+ let offset = 0;
145
+ for (const chunk of chunks) {
146
+ result.set(chunk, offset);
147
+ offset += chunk.length;
148
+ }
149
+ return result;
150
+ }
151
+ // ─── provider ────────────────────────────────────────────────────────────────
152
+ export function createElevenLabsSpeechProvider() {
153
+ return {
154
+ async generate(options) {
155
+ const apiKey = process.env['ELEVENLABS_API_KEY'];
156
+ if (!apiKey) {
157
+ throw new Error('Missing ELEVENLABS_API_KEY. Run: egaki login --provider elevenlabs --key <key>');
158
+ }
159
+ const voiceId = options.voice || DEFAULT_VOICE_ID;
160
+ const outputFormat = resolveOutputFormat(options.outputFormat);
161
+ const body = {
162
+ text: options.text,
163
+ model_id: options.modelId,
164
+ };
165
+ if (options.language) {
166
+ body.language_code = options.language;
167
+ }
168
+ if (options.speed != null) {
169
+ body.voice_settings = { speed: options.speed };
170
+ }
171
+ const queryParams = new URLSearchParams({ output_format: outputFormat });
172
+ const url = `${ELEVENLABS_API_BASE}/v1/text-to-speech/${voiceId}/stream/with-timestamps?${queryParams}`;
173
+ const response = await fetch(url, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ 'xi-api-key': apiKey,
178
+ },
179
+ body: JSON.stringify(body),
180
+ signal: options.abortSignal,
181
+ });
182
+ if (!response.ok) {
183
+ const errorText = await response.text().catch(() => '');
184
+ throw new Error(`ElevenLabs TTS API error ${response.status}: ${errorText || response.statusText}`);
185
+ }
186
+ if (!response.body) {
187
+ throw new Error('ElevenLabs TTS stream response has no body');
188
+ }
189
+ const { audioChunks, alignment } = await parseElevenLabsStream(response.body);
190
+ const audio = concatUint8Arrays(audioChunks);
191
+ const timestamps = alignment ? groupCharactersToWords(alignment) : undefined;
192
+ return {
193
+ audio,
194
+ mediaType: formatToMediaType(outputFormat),
195
+ timestamps,
196
+ };
197
+ },
198
+ };
199
+ }