egaki 0.6.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 (271) hide show
  1. package/CHANGELOG.md +140 -9
  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.js → cli/cli.js} +626 -600
  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/cli/credentials.d.ts.map +1 -0
  22. package/dist/{credentials.js → cli/credentials.js} +20 -0
  23. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  24. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  25. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  26. package/dist/cli/generate.d.ts +140 -0
  27. package/dist/cli/generate.d.ts.map +1 -0
  28. package/dist/cli/generate.js +604 -0
  29. package/dist/cli/generate.test.d.ts +2 -0
  30. package/dist/cli/generate.test.d.ts.map +1 -0
  31. package/dist/cli/generate.test.js +131 -0
  32. package/dist/cli/login.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts +3 -0
  34. package/dist/cli/main.d.ts.map +1 -0
  35. package/dist/cli/main.js +7 -0
  36. package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
  37. package/dist/cli/model-catalog.d.ts.map +1 -0
  38. package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
  39. package/dist/{models.d.ts → cli/models.d.ts} +12 -5
  40. package/dist/cli/models.d.ts.map +1 -0
  41. package/dist/{models.js → cli/models.js} +58 -57
  42. package/dist/cli/open-browser.d.ts.map +1 -0
  43. package/dist/cli/openai-speech-provider.d.ts +3 -0
  44. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.js +28 -0
  46. package/dist/cli/separation-generate.d.ts +34 -0
  47. package/dist/cli/separation-generate.d.ts.map +1 -0
  48. package/dist/cli/separation-generate.js +117 -0
  49. package/dist/cli/speech-catalog.d.ts +35 -0
  50. package/dist/cli/speech-catalog.d.ts.map +1 -0
  51. package/dist/cli/speech-catalog.js +195 -0
  52. package/dist/cli/speech-generate.d.ts +86 -0
  53. package/dist/cli/speech-generate.d.ts.map +1 -0
  54. package/dist/cli/speech-generate.js +156 -0
  55. package/dist/cli/speech-models.d.ts +8 -0
  56. package/dist/cli/speech-models.d.ts.map +1 -0
  57. package/dist/cli/speech-models.js +109 -0
  58. package/dist/cli/speech-provider.test.d.ts +2 -0
  59. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  60. package/dist/cli/speech-provider.test.js +197 -0
  61. package/dist/cli/subscription.d.ts.map +1 -0
  62. package/dist/cli/transcription-catalog.d.ts +35 -0
  63. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.js +192 -0
  65. package/dist/cli/transcription-generate.d.ts +62 -0
  66. package/dist/cli/transcription-generate.d.ts.map +1 -0
  67. package/dist/cli/transcription-generate.js +144 -0
  68. package/dist/cli/transcription-models.d.ts +9 -0
  69. package/dist/cli/transcription-models.d.ts.map +1 -0
  70. package/dist/cli/transcription-models.js +100 -0
  71. package/dist/cli/transcription-providers.d.ts +22 -0
  72. package/dist/cli/transcription-providers.d.ts.map +1 -0
  73. package/dist/cli/transcription-providers.js +290 -0
  74. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  75. package/dist/cli/xai-auth.d.ts.map +1 -0
  76. package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
  77. package/dist/vite/angled-screen.d.ts +57 -0
  78. package/dist/vite/angled-screen.d.ts.map +1 -0
  79. package/dist/vite/angled-screen.js +115 -0
  80. package/dist/vite/app.d.ts +51 -0
  81. package/dist/vite/app.d.ts.map +1 -0
  82. package/dist/vite/app.js +150 -0
  83. package/dist/vite/bands-shader.d.ts +47 -0
  84. package/dist/vite/bands-shader.d.ts.map +1 -0
  85. package/dist/vite/bands-shader.js +267 -0
  86. package/dist/vite/code-block.d.ts +107 -0
  87. package/dist/vite/code-block.d.ts.map +1 -0
  88. package/dist/vite/code-block.js +641 -0
  89. package/dist/vite/components.d.ts +36 -0
  90. package/dist/vite/components.d.ts.map +1 -0
  91. package/dist/vite/components.js +113 -0
  92. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  93. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  94. package/dist/vite/dispersion-rings-shader.js +282 -0
  95. package/dist/vite/easing-curves.d.ts +129 -0
  96. package/dist/vite/easing-curves.d.ts.map +1 -0
  97. package/dist/vite/easing-curves.js +584 -0
  98. package/dist/vite/easing-curves.test.d.ts +2 -0
  99. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  100. package/dist/vite/easing-curves.test.js +200 -0
  101. package/dist/vite/generated-media-client.d.ts +35 -0
  102. package/dist/vite/generated-media-client.d.ts.map +1 -0
  103. package/dist/vite/generated-media-client.js +76 -0
  104. package/dist/vite/keyframes.d.ts +129 -0
  105. package/dist/vite/keyframes.d.ts.map +1 -0
  106. package/dist/vite/keyframes.js +194 -0
  107. package/dist/vite/layout-editor.d.ts +45 -0
  108. package/dist/vite/layout-editor.d.ts.map +1 -0
  109. package/dist/vite/layout-editor.js +587 -0
  110. package/dist/vite/layout-transition.d.ts +136 -0
  111. package/dist/vite/layout-transition.d.ts.map +1 -0
  112. package/dist/vite/layout-transition.js +581 -0
  113. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  114. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  115. package/dist/vite/liquid-gradient-shader.js +413 -0
  116. package/dist/vite/mdx-client.d.ts +15 -0
  117. package/dist/vite/mdx-client.d.ts.map +1 -0
  118. package/dist/vite/mdx-client.js +265 -0
  119. package/dist/vite/mdx-parse.d.ts +102 -0
  120. package/dist/vite/mdx-parse.d.ts.map +1 -0
  121. package/dist/vite/mdx-parse.js +251 -0
  122. package/dist/vite/mdx-provided-components.d.ts +18 -0
  123. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  124. package/dist/vite/mdx-provided-components.js +14 -0
  125. package/dist/vite/mdx-video.d.ts +327 -0
  126. package/dist/vite/mdx-video.d.ts.map +1 -0
  127. package/dist/vite/mdx-video.js +596 -0
  128. package/dist/vite/mdx-video.test.d.ts +9 -0
  129. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  130. package/dist/vite/mdx-video.test.js +1945 -0
  131. package/dist/vite/media-components.d.ts +45 -0
  132. package/dist/vite/media-components.d.ts.map +1 -0
  133. package/dist/vite/media-components.js +355 -0
  134. package/dist/vite/media-duration-store.d.ts +66 -0
  135. package/dist/vite/media-duration-store.d.ts.map +1 -0
  136. package/dist/vite/media-duration-store.js +204 -0
  137. package/dist/vite/player-page.d.ts +35 -0
  138. package/dist/vite/player-page.d.ts.map +1 -0
  139. package/dist/vite/player-page.js +720 -0
  140. package/dist/vite/render-client.d.ts +18 -0
  141. package/dist/vite/render-client.d.ts.map +1 -0
  142. package/dist/vite/render-client.js +67 -0
  143. package/dist/vite/sdk.d.ts +221 -0
  144. package/dist/vite/sdk.d.ts.map +1 -0
  145. package/dist/vite/sdk.js +355 -0
  146. package/dist/vite/server-components.d.ts +55 -0
  147. package/dist/vite/server-components.d.ts.map +1 -0
  148. package/dist/vite/server-components.js +156 -0
  149. package/dist/vite/server-mdx.d.ts +74 -0
  150. package/dist/vite/server-mdx.d.ts.map +1 -0
  151. package/dist/vite/server-mdx.js +259 -0
  152. package/dist/vite/shader-renderer.d.ts +48 -0
  153. package/dist/vite/shader-renderer.d.ts.map +1 -0
  154. package/dist/vite/shader-renderer.js +411 -0
  155. package/dist/vite/store-hooks.d.ts +12 -0
  156. package/dist/vite/store-hooks.d.ts.map +1 -0
  157. package/dist/vite/store-hooks.js +93 -0
  158. package/dist/vite/store.d.ts +75 -0
  159. package/dist/vite/store.d.ts.map +1 -0
  160. package/dist/vite/store.js +52 -0
  161. package/dist/vite/store.test.d.ts +6 -0
  162. package/dist/vite/store.test.d.ts.map +1 -0
  163. package/dist/vite/store.test.js +172 -0
  164. package/dist/vite/tweakpane-hook.d.ts +94 -0
  165. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  166. package/dist/vite/tweakpane-hook.js +450 -0
  167. package/dist/vite/vite-plugin.d.ts +28 -0
  168. package/dist/vite/vite-plugin.d.ts.map +1 -0
  169. package/dist/vite/vite-plugin.js +538 -0
  170. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  171. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  172. package/dist/vite/wave-gradient-shader.js +207 -0
  173. package/package.json +101 -24
  174. package/skills/egaki/SKILL.md +162 -1
  175. package/src/cli/cache-utils.ts +169 -0
  176. package/src/cli/cached-generate.test.ts +531 -0
  177. package/src/cli/cached-generate.ts +348 -0
  178. package/src/cli/cartesia-provider.ts +249 -0
  179. package/src/{cli.ts → cli/cli.ts} +836 -884
  180. package/src/cli/clone-generate.ts +202 -0
  181. package/src/{credentials.ts → cli/credentials.ts} +20 -0
  182. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  183. package/src/cli/generate.test.ts +166 -0
  184. package/src/cli/generate.ts +912 -0
  185. package/src/cli/main.ts +8 -0
  186. package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
  187. package/src/{models.ts → cli/models.ts} +65 -86
  188. package/src/cli/openai-speech-provider.ts +34 -0
  189. package/src/cli/separation-generate.ts +187 -0
  190. package/src/cli/speech-catalog.ts +253 -0
  191. package/src/cli/speech-generate.ts +275 -0
  192. package/src/cli/speech-models.ts +146 -0
  193. package/src/cli/speech-provider.test.ts +226 -0
  194. package/src/cli/transcription-catalog.ts +250 -0
  195. package/src/cli/transcription-generate.ts +214 -0
  196. package/src/cli/transcription-models.ts +136 -0
  197. package/src/cli/transcription-providers.ts +427 -0
  198. package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
  199. package/src/vite/angled-screen.tsx +214 -0
  200. package/src/vite/app.tsx +197 -0
  201. package/src/vite/bands-shader.tsx +319 -0
  202. package/src/vite/code-block.tsx +939 -0
  203. package/src/vite/components.tsx +278 -0
  204. package/src/vite/dispersion-rings-shader.tsx +331 -0
  205. package/src/vite/easing-curves.test.ts +232 -0
  206. package/src/vite/easing-curves.ts +743 -0
  207. package/src/vite/generated-media-client.tsx +121 -0
  208. package/src/vite/keyframes.tsx +347 -0
  209. package/src/vite/layout-editor.tsx +709 -0
  210. package/src/vite/layout-transition.tsx +706 -0
  211. package/src/vite/liquid-gradient-shader.tsx +474 -0
  212. package/src/vite/mdx-client.tsx +420 -0
  213. package/src/vite/mdx-parse.ts +343 -0
  214. package/src/vite/mdx-provided-components.ts +19 -0
  215. package/src/vite/mdx-video.test.tsx +2219 -0
  216. package/src/vite/mdx-video.tsx +976 -0
  217. package/src/vite/media-components.tsx +473 -0
  218. package/src/vite/media-duration-store.ts +225 -0
  219. package/src/vite/motion-timing.ts +194 -0
  220. package/src/vite/player-page.tsx +1107 -0
  221. package/src/vite/render-client.ts +84 -0
  222. package/src/vite/sdk.ts +598 -0
  223. package/src/vite/server-components.tsx +241 -0
  224. package/src/vite/server-mdx.ts +285 -0
  225. package/src/vite/shader-renderer.tsx +535 -0
  226. package/src/vite/store-hooks.ts +117 -0
  227. package/src/vite/store.test.ts +202 -0
  228. package/src/vite/store.ts +115 -0
  229. package/src/vite/styles.css +21 -0
  230. package/src/vite/tweakpane-hook.tsx +593 -0
  231. package/src/vite/virtual-modules.d.ts +30 -0
  232. package/src/vite/vite-plugin.ts +584 -0
  233. package/src/vite/wave-gradient-shader.tsx +253 -0
  234. package/README.md +0 -370
  235. package/dist/antigravity-auth.d.ts +0 -15
  236. package/dist/antigravity-auth.d.ts.map +0 -1
  237. package/dist/antigravity-auth.js +0 -544
  238. package/dist/antigravity-image.d.ts +0 -11
  239. package/dist/antigravity-image.d.ts.map +0 -1
  240. package/dist/antigravity-image.js +0 -285
  241. package/dist/chatgpt-auth.d.ts.map +0 -1
  242. package/dist/cli.d.ts +0 -3
  243. package/dist/cli.d.ts.map +0 -1
  244. package/dist/credentials.d.ts.map +0 -1
  245. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  246. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  247. package/dist/gateway-model-candidates.generated.js +0 -64
  248. package/dist/login.d.ts.map +0 -1
  249. package/dist/model-catalog.d.ts.map +0 -1
  250. package/dist/models.d.ts.map +0 -1
  251. package/dist/open-browser.d.ts.map +0 -1
  252. package/dist/subscription.d.ts.map +0 -1
  253. package/dist/video-model-catalog.d.ts.map +0 -1
  254. package/dist/xai-auth.d.ts.map +0 -1
  255. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  256. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  257. /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
  258. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  259. /package/dist/{login.js → cli/login.js} +0 -0
  260. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  261. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  262. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  263. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  264. /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
  265. /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
  266. /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
  267. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  268. /package/src/{login.ts → cli/login.ts} +0 -0
  269. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  270. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
  271. /package/src/{video-model-catalog.ts → cli/video-model-catalog.ts} +0 -0
@@ -0,0 +1,136 @@
1
+ // Transcription model registry for egaki.
2
+ // Maps transcription model IDs to their provider and direct HTTP provider factory.
3
+ // Follows the same pattern as speech-models.ts: provider factory map with lazy
4
+ // imports, auth source detection, and key validation.
5
+ //
6
+ // No AI SDK dependency — all providers use direct HTTP calls via
7
+ // TranscriptionProvider implementations in transcription-providers.ts.
8
+ import pc from 'picocolors'
9
+ import type { TranscriptionProvider } from './transcription-providers.js'
10
+ import {
11
+ PROVIDERS,
12
+ injectCredentialsToEnv,
13
+ } from './credentials.js'
14
+ import { ValidationError } from './models.js'
15
+ import {
16
+ TRANSCRIPTION_CATALOG,
17
+ findTranscriptionModel,
18
+ type TranscriptionModelEntry,
19
+ } from './transcription-catalog.js'
20
+
21
+ export { type TranscriptionModelEntry }
22
+
23
+ export const TRANSCRIPTION_MODELS = TRANSCRIPTION_CATALOG.map((m) => m.id) as [string, ...string[]]
24
+ export const DEFAULT_TRANSCRIPTION_MODEL = 'whisper-1'
25
+
26
+ // ─── provider factory map ────────────────────────────────────────────────────
27
+
28
+ type TranscriptionProviderFactory = () => Promise<TranscriptionProvider>
29
+
30
+ const TRANSCRIPTION_PROVIDER_FACTORIES: Record<string, TranscriptionProviderFactory> = {
31
+ openai: async () => {
32
+ const { createOpenAITranscriptionProvider } = await import('./transcription-providers.js')
33
+ return createOpenAITranscriptionProvider()
34
+ },
35
+ elevenlabs: async () => {
36
+ const { createElevenLabsTranscriptionProvider } = await import('./transcription-providers.js')
37
+ return createElevenLabsTranscriptionProvider()
38
+ },
39
+ deepgram: async () => {
40
+ const { createDeepgramTranscriptionProvider } = await import('./transcription-providers.js')
41
+ return createDeepgramTranscriptionProvider()
42
+ },
43
+ groq: async () => {
44
+ const { createGroqTranscriptionProvider } = await import('./transcription-providers.js')
45
+ return createGroqTranscriptionProvider()
46
+ },
47
+ cartesia: async () => {
48
+ const { createCartesiaTranscriptionProvider } = await import('./transcription-providers.js')
49
+ return createCartesiaTranscriptionProvider()
50
+ },
51
+ }
52
+
53
+ // ─── key checking ────────────────────────────────────────────────────────────
54
+
55
+ function hasDirectProviderKey(providerName: string): boolean {
56
+ const info = PROVIDERS[providerName]
57
+ if (!info) return false
58
+ return Boolean(process.env[info.envVar])
59
+ }
60
+
61
+ // ─── config lookup ───────────────────────────────────────────────────────────
62
+
63
+ export function getTranscriptionModelConfig(modelId: string): Error | TranscriptionModelEntry {
64
+ const entry = findTranscriptionModel(modelId)
65
+ if (!entry) {
66
+ return new ValidationError(`Unknown transcription model: ${modelId}`)
67
+ }
68
+ return entry
69
+ }
70
+
71
+ // ─── auth source detection ───────────────────────────────────────────────────
72
+
73
+ type AuthSource =
74
+ | { type: 'api-key'; label: string }
75
+
76
+ function resolveAuthSource(providerName: string): AuthSource {
77
+ const info = PROVIDERS[providerName]
78
+ if (info && process.env[info.envVar]) {
79
+ return { type: 'api-key', label: info.envVar }
80
+ }
81
+ return { type: 'api-key', label: 'unknown' }
82
+ }
83
+
84
+ function logAuthSource(source: AuthSource): void {
85
+ console.error(pc.dim(`Auth: ${source.label}`))
86
+ }
87
+
88
+ // ─── ensure key ──────────────────────────────────────────────────────────────
89
+
90
+ export function ensureTranscriptionProviderKey(providerName: string): Error | undefined {
91
+ if (hasDirectProviderKey(providerName)) return undefined
92
+
93
+ const info = PROVIDERS[providerName]
94
+ const label = info?.label || providerName
95
+ return new ValidationError(
96
+ `Missing API key for ${label}. ` +
97
+ `Run: egaki login --provider ${providerName} --key <key>. ` +
98
+ (info ? info.hint : ''),
99
+ )
100
+ }
101
+
102
+ // ─── provider cache ──────────────────────────────────────────────────────────
103
+
104
+ /** Global cache of provider instances, keyed by provider name. */
105
+ const providerCache: Map<string, Promise<TranscriptionProvider>> =
106
+ (globalThis as any).__egakiTranscriptionProviderCache ??= new Map()
107
+
108
+ // ─── public factory ──────────────────────────────────────────────────────────
109
+
110
+ export function getTranscriptionProvider(providerName: string): Error | TranscriptionProvider {
111
+ injectCredentialsToEnv()
112
+
113
+ const keyError = ensureTranscriptionProviderKey(providerName)
114
+ if (keyError) return keyError
115
+
116
+ const authSource = resolveAuthSource(providerName)
117
+ logAuthSource(authSource)
118
+
119
+ const factory = TRANSCRIPTION_PROVIDER_FACTORIES[providerName]
120
+ if (!factory) {
121
+ return new Error(`Transcription is not supported for provider: ${providerName}`)
122
+ }
123
+
124
+ // Return a lazy provider: the factory is async but we want getTranscriptionProvider
125
+ // to be sync for ergonomic use in transcribeAudioUncached. The provider promise
126
+ // is cached globally per provider name so the factory runs once across all calls.
127
+ return {
128
+ async transcribe(options) {
129
+ const cached = providerCache.get(providerName)
130
+ if (cached) return (await cached).transcribe(options)
131
+ const promise = factory()
132
+ providerCache.set(providerName, promise)
133
+ return (await promise).transcribe(options)
134
+ },
135
+ }
136
+ }
@@ -0,0 +1,427 @@
1
+ // Direct HTTP transcription providers for egaki.
2
+ // Each provider sends multipart form or raw audio to the provider API
3
+ // and parses the response, including word-level timestamps.
4
+ //
5
+ // This replaces the AI SDK's experimental_transcribe + @ai-sdk/* provider
6
+ // packages, giving us full control over request/response handling.
7
+ // In particular, we can:
8
+ // - Send verbose_json + word timestamp granularities for whisper-1
9
+ // - Avoid verbose_json for gpt-4o-transcribe models, which only support json/text
10
+ // - Parse the top-level `words` array from Groq responses (the AI SDK
11
+ // Groq provider only parses `segments`)
12
+ //
13
+ // API reference / OpenAPI specs:
14
+ // OpenAI: https://platform.openai.com/docs/api-reference/audio/createTranscription
15
+ // Groq: https://console.groq.com/docs/api-reference#audio-transcription
16
+ // (OpenAI-compatible endpoint at /openai/v1/audio/transcriptions)
17
+ // ElevenLabs: https://elevenlabs.io/docs/api-reference/speech-to-text/convert
18
+ // Deepgram: https://developers.deepgram.com/reference/listen-file
19
+
20
+ import type { TranscriptionSegment } from './transcription-generate.js'
21
+
22
+ // ─── shared types ────────────────────────────────────────────────────────────
23
+
24
+ export interface TranscriptionProviderOptions {
25
+ audio: Uint8Array
26
+ modelId: string
27
+ language?: string
28
+ abortSignal?: AbortSignal
29
+ }
30
+
31
+ export interface TranscriptionProviderResult {
32
+ text: string
33
+ segments: TranscriptionSegment[]
34
+ language?: string
35
+ durationInSeconds?: number
36
+ }
37
+
38
+ export interface TranscriptionProvider {
39
+ transcribe(options: TranscriptionProviderOptions): Promise<TranscriptionProviderResult>
40
+ }
41
+
42
+ // ─── shared helpers ──────────────────────────────────────────────────────────
43
+
44
+ function mediaTypeFromAudioBytes(audio: Uint8Array): string {
45
+ if (audio.length >= 3 && audio[0] === 0x49 && audio[1] === 0x44 && audio[2] === 0x33) return 'audio/mpeg' // ID3 header
46
+ if (audio.length >= 2 && audio[0] === 0xff && (audio[1]! & 0xe0) === 0xe0) return 'audio/mpeg' // MP3 sync word
47
+ // MP4/M4A: ftyp box at byte offset 4 (box size in first 4 bytes, then 'ftyp')
48
+ if (audio.length >= 12 && audio[4] === 0x66 && audio[5] === 0x74 && audio[6] === 0x79 && audio[7] === 0x70) return 'audio/mp4'
49
+ // WebM: EBML header magic bytes
50
+ if (audio.length >= 4 && audio[0] === 0x1a && audio[1] === 0x45 && audio[2] === 0xdf && audio[3] === 0xa3) return 'audio/webm'
51
+ if (audio.length >= 4 && audio[0] === 0x52 && audio[1] === 0x49 && audio[2] === 0x46 && audio[3] === 0x46) return 'audio/wav' // RIFF
52
+ if (audio.length >= 4 && audio[0] === 0x4f && audio[1] === 0x67 && audio[2] === 0x67 && audio[3] === 0x53) return 'audio/ogg' // OggS
53
+ if (audio.length >= 4 && audio[0] === 0x66 && audio[1] === 0x4c && audio[2] === 0x61 && audio[3] === 0x43) return 'audio/flac' // fLaC
54
+ return 'audio/mpeg'
55
+ }
56
+
57
+ function extensionFromMediaType(mediaType: string): string {
58
+ const map: Record<string, string> = {
59
+ 'audio/mpeg': 'mp3',
60
+ 'audio/mp3': 'mp3',
61
+ 'audio/wav': 'wav',
62
+ 'audio/x-wav': 'wav',
63
+ 'audio/ogg': 'ogg',
64
+ 'audio/flac': 'flac',
65
+ 'audio/webm': 'webm',
66
+ 'audio/mp4': 'm4a',
67
+ 'audio/m4a': 'm4a',
68
+ 'audio/x-m4a': 'm4a',
69
+ }
70
+ return map[mediaType] || 'mp3'
71
+ }
72
+
73
+ function buildAudioFormData(audio: Uint8Array, modelId: string): { formData: FormData; mediaType: string } {
74
+ const mediaType = mediaTypeFromAudioBytes(audio)
75
+ const ext = extensionFromMediaType(mediaType)
76
+ const formData = new FormData()
77
+ // Copy into a fresh ArrayBuffer to satisfy TypeScript's BlobPart constraint
78
+ // (Uint8Array backed by SharedArrayBuffer is not assignable to BlobPart).
79
+ const buf = new Uint8Array(audio) as BlobPart
80
+ const blob = new Blob([buf], { type: mediaType })
81
+ formData.append('file', new File([blob], `audio.${ext}`, { type: mediaType }), `audio.${ext}`)
82
+ formData.append('model', modelId)
83
+ return { formData, mediaType }
84
+ }
85
+
86
+ // ─── OpenAI ──────────────────────────────────────────────────────────────────
87
+ // POST https://api.openai.com/v1/audio/transcriptions
88
+ // Multipart form: file, model, response_format, timestamp_granularities[], language
89
+ //
90
+ // gpt-4o-transcribe and gpt-4o-mini-transcribe only support response_format=json
91
+ // (NOT verbose_json). Word timestamps require verbose_json, so these models
92
+ // cannot return word-level timestamps. This is an OpenAI API limitation, not ours.
93
+ // whisper-1 supports verbose_json and returns word timestamps.
94
+
95
+ const GPT4O_TRANSCRIBE_MODELS = ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe']
96
+
97
+ interface OpenAITranscriptionResponse {
98
+ text: string
99
+ language?: string
100
+ duration?: number
101
+ segments?: Array<{ text: string; start: number; end: number }>
102
+ words?: Array<{ word: string; start: number; end: number }>
103
+ }
104
+
105
+ export function createOpenAITranscriptionProvider(): TranscriptionProvider {
106
+ return {
107
+ async transcribe(options) {
108
+ const apiKey = process.env['OPENAI_API_KEY']
109
+ if (!apiKey) {
110
+ throw new Error('Missing OPENAI_API_KEY. Run: egaki login --provider openai --key <key>')
111
+ }
112
+
113
+ const { formData } = buildAudioFormData(options.audio, options.modelId)
114
+
115
+ // gpt-4o-transcribe models only support "json" format;
116
+ // whisper-1 supports "verbose_json" which enables word timestamps.
117
+ const isGpt4oTranscribe = GPT4O_TRANSCRIBE_MODELS.includes(options.modelId)
118
+ formData.append('response_format', isGpt4oTranscribe ? 'json' : 'verbose_json')
119
+ if (!isGpt4oTranscribe) {
120
+ formData.append('timestamp_granularities[]', 'word')
121
+ }
122
+ if (options.language) {
123
+ formData.append('language', options.language)
124
+ }
125
+
126
+ const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
127
+ method: 'POST',
128
+ headers: { 'Authorization': `Bearer ${apiKey}` },
129
+ body: formData,
130
+ signal: options.abortSignal,
131
+ })
132
+
133
+ if (!response.ok) {
134
+ const errorText = await response.text().catch(() => '')
135
+ throw new Error(`OpenAI transcription API error ${response.status}: ${errorText || response.statusText}`)
136
+ }
137
+
138
+ const json = await response.json() as OpenAITranscriptionResponse
139
+
140
+ // Prefer word-level segments from the `words` array; fall back to
141
+ // sentence-level `segments` if words are not present.
142
+ const segments: TranscriptionSegment[] =
143
+ json.words?.map((w) => ({ text: w.word, startSecond: w.start, endSecond: w.end }))
144
+ ?? json.segments?.map((s) => ({ text: s.text, startSecond: s.start, endSecond: s.end }))
145
+ ?? []
146
+
147
+ return {
148
+ text: json.text,
149
+ segments,
150
+ language: json.language,
151
+ durationInSeconds: json.duration,
152
+ }
153
+ },
154
+ }
155
+ }
156
+
157
+ // ─── Groq ────────────────────────────────────────────────────────────────────
158
+ // POST https://api.groq.com/openai/v1/audio/transcriptions
159
+ // OpenAI-compatible multipart form. The key fix: we parse the top-level `words`
160
+ // array from the response, which the AI SDK's @ai-sdk/groq provider ignores
161
+ // (it only parses `segments`).
162
+
163
+ interface GroqTranscriptionResponse {
164
+ text: string
165
+ language?: string
166
+ duration?: number
167
+ segments?: Array<{ text: string; start: number; end: number }>
168
+ words?: Array<{ word: string; start: number; end: number }>
169
+ }
170
+
171
+ export function createGroqTranscriptionProvider(): TranscriptionProvider {
172
+ return {
173
+ async transcribe(options) {
174
+ const apiKey = process.env['GROQ_API_KEY']
175
+ if (!apiKey) {
176
+ throw new Error('Missing GROQ_API_KEY. Run: egaki login --provider groq --key <key>')
177
+ }
178
+
179
+ const { formData } = buildAudioFormData(options.audio, options.modelId)
180
+ formData.append('response_format', 'verbose_json')
181
+ formData.append('timestamp_granularities[]', 'word')
182
+ formData.append('timestamp_granularities[]', 'segment')
183
+ if (options.language) {
184
+ formData.append('language', options.language)
185
+ }
186
+
187
+ const response = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', {
188
+ method: 'POST',
189
+ headers: { 'Authorization': `Bearer ${apiKey}` },
190
+ body: formData,
191
+ signal: options.abortSignal,
192
+ })
193
+
194
+ if (!response.ok) {
195
+ const errorText = await response.text().catch(() => '')
196
+ throw new Error(`Groq transcription API error ${response.status}: ${errorText || response.statusText}`)
197
+ }
198
+
199
+ const json = await response.json() as GroqTranscriptionResponse
200
+
201
+ // Prefer word-level from the `words` array (now parsed correctly);
202
+ // fall back to sentence-level `segments`.
203
+ const segments: TranscriptionSegment[] =
204
+ json.words?.map((w) => ({ text: w.word, startSecond: w.start, endSecond: w.end }))
205
+ ?? json.segments?.map((s) => ({ text: s.text, startSecond: s.start, endSecond: s.end }))
206
+ ?? []
207
+
208
+ return {
209
+ text: json.text,
210
+ segments,
211
+ language: json.language,
212
+ durationInSeconds: json.duration,
213
+ }
214
+ },
215
+ }
216
+ }
217
+
218
+ // ─── ElevenLabs ──────────────────────────────────────────────────────────────
219
+ // POST https://api.elevenlabs.io/v1/speech-to-text
220
+ // Multipart form: file, model_id, language_code, timestamps_granularity, tag_audio_events
221
+ // Returns JSON with text, language_code, words[{ text, type, start, end }]
222
+
223
+ interface ElevenLabsTranscriptionResponse {
224
+ text: string
225
+ language_code: string
226
+ language_probability: number
227
+ words?: Array<{
228
+ text: string
229
+ type: 'word' | 'spacing' | 'audio_event'
230
+ start?: number
231
+ end?: number
232
+ }>
233
+ }
234
+
235
+ export function createElevenLabsTranscriptionProvider(): TranscriptionProvider {
236
+ return {
237
+ async transcribe(options) {
238
+ const apiKey = process.env['ELEVENLABS_API_KEY']
239
+ if (!apiKey) {
240
+ throw new Error('Missing ELEVENLABS_API_KEY. Run: egaki login --provider elevenlabs --key <key>')
241
+ }
242
+
243
+ const mediaType = mediaTypeFromAudioBytes(options.audio)
244
+ const ext = extensionFromMediaType(mediaType)
245
+ const formData = new FormData()
246
+ const buf = new Uint8Array(options.audio) as BlobPart
247
+ const blob = new Blob([buf], { type: mediaType })
248
+ formData.append('file', new File([blob], `audio.${ext}`, { type: mediaType }), `audio.${ext}`)
249
+ formData.append('model_id', options.modelId)
250
+ formData.append('timestamps_granularity', 'word')
251
+ formData.append('tag_audio_events', 'true')
252
+ if (options.language) {
253
+ formData.append('language_code', options.language)
254
+ }
255
+
256
+ const response = await fetch('https://api.elevenlabs.io/v1/speech-to-text', {
257
+ method: 'POST',
258
+ headers: { 'xi-api-key': apiKey },
259
+ body: formData,
260
+ signal: options.abortSignal,
261
+ })
262
+
263
+ if (!response.ok) {
264
+ const errorText = await response.text().catch(() => '')
265
+ throw new Error(`ElevenLabs transcription API error ${response.status}: ${errorText || response.statusText}`)
266
+ }
267
+
268
+ const json = await response.json() as ElevenLabsTranscriptionResponse
269
+
270
+ // Filter to only 'word' type entries (skip 'spacing' and 'audio_event')
271
+ const segments: TranscriptionSegment[] = json.words
272
+ ?.filter((w) => w.type === 'word' && w.text.trim())
273
+ .map((w) => ({ text: w.text, startSecond: w.start ?? 0, endSecond: w.end ?? 0 }))
274
+ ?? []
275
+
276
+ // Duration is the end time of the last word
277
+ const lastWord = json.words?.findLast((w) => w.type === 'word' && w.end != null)
278
+ const durationInSeconds = lastWord?.end ?? undefined
279
+
280
+ return {
281
+ text: json.text,
282
+ segments,
283
+ language: json.language_code,
284
+ durationInSeconds,
285
+ }
286
+ },
287
+ }
288
+ }
289
+
290
+ // ─── Deepgram ────────────────────────────────────────────────────────────────
291
+ // POST https://api.deepgram.com/v1/listen?model={model}&...
292
+ // Raw audio body with Content-Type header (not multipart form).
293
+ // Query params control model, language, detect_language, smart_format, etc.
294
+ // Returns results.channels[0].alternatives[0].{ transcript, words[{ word, start, end }] }
295
+ // and metadata.duration for audio length.
296
+
297
+ interface DeepgramTranscriptionResponse {
298
+ metadata?: { duration?: number }
299
+ results?: {
300
+ channels: Array<{
301
+ detected_language?: string
302
+ alternatives: Array<{
303
+ transcript: string
304
+ words: Array<{ word: string; start: number; end: number }>
305
+ }>
306
+ }>
307
+ }
308
+ }
309
+
310
+ export function createDeepgramTranscriptionProvider(): TranscriptionProvider {
311
+ return {
312
+ async transcribe(options) {
313
+ const apiKey = process.env['DEEPGRAM_API_KEY']
314
+ if (!apiKey) {
315
+ throw new Error('Missing DEEPGRAM_API_KEY. Run: egaki login --provider deepgram --key <key>')
316
+ }
317
+
318
+ const mediaType = mediaTypeFromAudioBytes(options.audio)
319
+
320
+ const params = new URLSearchParams()
321
+ params.append('model', options.modelId)
322
+ params.append('smart_format', 'true')
323
+ if (options.language) {
324
+ params.append('language', options.language)
325
+ } else {
326
+ params.append('detect_language', 'true')
327
+ }
328
+
329
+ const response = await fetch(`https://api.deepgram.com/v1/listen?${params}`, {
330
+ method: 'POST',
331
+ headers: {
332
+ 'Authorization': `Token ${apiKey}`,
333
+ 'Content-Type': mediaType,
334
+ },
335
+ body: new Uint8Array(options.audio) as any,
336
+ signal: options.abortSignal,
337
+ })
338
+
339
+ if (!response.ok) {
340
+ const errorText = await response.text().catch(() => '')
341
+ throw new Error(`Deepgram transcription API error ${response.status}: ${errorText || response.statusText}`)
342
+ }
343
+
344
+ const json = await response.json() as DeepgramTranscriptionResponse
345
+
346
+ const channel = json.results?.channels?.[0]
347
+ const alt = channel?.alternatives?.[0]
348
+
349
+ const segments: TranscriptionSegment[] =
350
+ alt?.words?.map((w) => ({ text: w.word, startSecond: w.start, endSecond: w.end }))
351
+ ?? []
352
+
353
+ return {
354
+ text: alt?.transcript ?? '',
355
+ segments,
356
+ language: channel?.detected_language,
357
+ durationInSeconds: json.metadata?.duration,
358
+ }
359
+ },
360
+ }
361
+ }
362
+
363
+ // ─── Cartesia ────────────────────────────────────────────────────────────────
364
+ // POST https://api.cartesia.ai/stt
365
+ // Already implemented in cartesia-provider.ts with direct HTTP calls.
366
+ // This wrapper adapts the existing implementation to the TranscriptionProvider
367
+ // interface.
368
+
369
+ interface CartesiaSTTResponse {
370
+ text: string
371
+ type: 'transcript'
372
+ duration?: number
373
+ language?: string
374
+ words?: Array<{ word: string; start: number; end: number }>
375
+ }
376
+
377
+ const CARTESIA_API_BASE = 'https://api.cartesia.ai'
378
+ const CARTESIA_API_VERSION = '2026-03-01'
379
+
380
+ export function createCartesiaTranscriptionProvider(): TranscriptionProvider {
381
+ return {
382
+ async transcribe(options) {
383
+ const apiKey = process.env['CARTESIA_API_KEY']
384
+ if (!apiKey) {
385
+ throw new Error('Missing CARTESIA_API_KEY. Run: egaki login --provider cartesia --key <key>')
386
+ }
387
+
388
+ const mediaType = mediaTypeFromAudioBytes(options.audio)
389
+ const ext = extensionFromMediaType(mediaType)
390
+ const formData = new FormData()
391
+ const buf = new Uint8Array(options.audio) as BlobPart
392
+ const blob = new Blob([buf], { type: mediaType })
393
+ formData.append('file', new File([blob], `audio.${ext}`, { type: mediaType }), `audio.${ext}`)
394
+ formData.append('model', options.modelId)
395
+ formData.append('language', options.language || 'en')
396
+ formData.append('timestamp_granularities[]', 'word')
397
+
398
+ const response = await fetch(`${CARTESIA_API_BASE}/stt`, {
399
+ method: 'POST',
400
+ headers: {
401
+ 'Cartesia-Version': CARTESIA_API_VERSION,
402
+ 'Authorization': `Bearer ${apiKey}`,
403
+ },
404
+ body: formData,
405
+ signal: options.abortSignal,
406
+ })
407
+
408
+ if (!response.ok) {
409
+ const errorText = await response.text().catch(() => '')
410
+ throw new Error(`Cartesia STT API error ${response.status}: ${errorText || response.statusText}`)
411
+ }
412
+
413
+ const json = await response.json() as CartesiaSTTResponse
414
+
415
+ const segments: TranscriptionSegment[] =
416
+ json.words?.map((w) => ({ text: w.word, startSecond: w.start, endSecond: w.end }))
417
+ ?? []
418
+
419
+ return {
420
+ text: json.text,
421
+ segments,
422
+ language: json.language,
423
+ durationInSeconds: json.duration,
424
+ }
425
+ },
426
+ }
427
+ }
@@ -15,7 +15,7 @@ import { spinner, log, note } from '@clack/prompts'
15
15
  import pc from 'picocolors'
16
16
  import { z } from 'zod'
17
17
  import { openUrlInBrowser } from './open-browser.js'
18
- import pkg from '../package.json' with { type: 'json' }
18
+ import pkg from '../../package.json' with { type: 'json' }
19
19
 
20
20
  // Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
21
21
  // non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships