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.
- package/CHANGELOG.md +140 -9
- package/dist/cli/cache-utils.d.ts +33 -0
- package/dist/cli/cache-utils.d.ts.map +1 -0
- package/dist/cli/cache-utils.js +157 -0
- package/dist/cli/cached-generate.d.ts +105 -0
- package/dist/cli/cached-generate.d.ts.map +1 -0
- package/dist/cli/cached-generate.js +208 -0
- package/dist/cli/cached-generate.test.d.ts +11 -0
- package/dist/cli/cached-generate.test.d.ts.map +1 -0
- package/dist/cli/cached-generate.test.js +431 -0
- package/dist/cli/cartesia-provider.d.ts +16 -0
- package/dist/cli/cartesia-provider.d.ts.map +1 -0
- package/dist/cli/cartesia-provider.js +184 -0
- package/dist/cli/chatgpt-auth.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/{cli.js → cli/cli.js} +626 -600
- package/dist/cli/clone-generate.d.ts +26 -0
- package/dist/cli/clone-generate.d.ts.map +1 -0
- package/dist/cli/clone-generate.js +148 -0
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/{credentials.js → cli/credentials.js} +20 -0
- package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
- package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
- package/dist/cli/elevenlabs-speech-provider.js +199 -0
- package/dist/cli/generate.d.ts +140 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +604 -0
- package/dist/cli/generate.test.d.ts +2 -0
- package/dist/cli/generate.test.d.ts.map +1 -0
- package/dist/cli/generate.test.js +131 -0
- package/dist/cli/login.d.ts.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +7 -0
- package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
- package/dist/cli/model-catalog.d.ts.map +1 -0
- package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
- package/dist/{models.d.ts → cli/models.d.ts} +12 -5
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/{models.js → cli/models.js} +58 -57
- package/dist/cli/open-browser.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.d.ts +3 -0
- package/dist/cli/openai-speech-provider.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.js +28 -0
- package/dist/cli/separation-generate.d.ts +34 -0
- package/dist/cli/separation-generate.d.ts.map +1 -0
- package/dist/cli/separation-generate.js +117 -0
- package/dist/cli/speech-catalog.d.ts +35 -0
- package/dist/cli/speech-catalog.d.ts.map +1 -0
- package/dist/cli/speech-catalog.js +195 -0
- package/dist/cli/speech-generate.d.ts +86 -0
- package/dist/cli/speech-generate.d.ts.map +1 -0
- package/dist/cli/speech-generate.js +156 -0
- package/dist/cli/speech-models.d.ts +8 -0
- package/dist/cli/speech-models.d.ts.map +1 -0
- package/dist/cli/speech-models.js +109 -0
- package/dist/cli/speech-provider.test.d.ts +2 -0
- package/dist/cli/speech-provider.test.d.ts.map +1 -0
- package/dist/cli/speech-provider.test.js +197 -0
- package/dist/cli/subscription.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.d.ts +35 -0
- package/dist/cli/transcription-catalog.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.js +192 -0
- package/dist/cli/transcription-generate.d.ts +62 -0
- package/dist/cli/transcription-generate.d.ts.map +1 -0
- package/dist/cli/transcription-generate.js +144 -0
- package/dist/cli/transcription-models.d.ts +9 -0
- package/dist/cli/transcription-models.d.ts.map +1 -0
- package/dist/cli/transcription-models.js +100 -0
- package/dist/cli/transcription-providers.d.ts +22 -0
- package/dist/cli/transcription-providers.d.ts.map +1 -0
- package/dist/cli/transcription-providers.js +290 -0
- package/dist/cli/video-model-catalog.d.ts.map +1 -0
- package/dist/cli/xai-auth.d.ts.map +1 -0
- package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
- package/dist/vite/angled-screen.d.ts +57 -0
- package/dist/vite/angled-screen.d.ts.map +1 -0
- package/dist/vite/angled-screen.js +115 -0
- package/dist/vite/app.d.ts +51 -0
- package/dist/vite/app.d.ts.map +1 -0
- package/dist/vite/app.js +150 -0
- package/dist/vite/bands-shader.d.ts +47 -0
- package/dist/vite/bands-shader.d.ts.map +1 -0
- package/dist/vite/bands-shader.js +267 -0
- package/dist/vite/code-block.d.ts +107 -0
- package/dist/vite/code-block.d.ts.map +1 -0
- package/dist/vite/code-block.js +641 -0
- package/dist/vite/components.d.ts +36 -0
- package/dist/vite/components.d.ts.map +1 -0
- package/dist/vite/components.js +113 -0
- package/dist/vite/dispersion-rings-shader.d.ts +44 -0
- package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
- package/dist/vite/dispersion-rings-shader.js +282 -0
- package/dist/vite/easing-curves.d.ts +129 -0
- package/dist/vite/easing-curves.d.ts.map +1 -0
- package/dist/vite/easing-curves.js +584 -0
- package/dist/vite/easing-curves.test.d.ts +2 -0
- package/dist/vite/easing-curves.test.d.ts.map +1 -0
- package/dist/vite/easing-curves.test.js +200 -0
- package/dist/vite/generated-media-client.d.ts +35 -0
- package/dist/vite/generated-media-client.d.ts.map +1 -0
- package/dist/vite/generated-media-client.js +76 -0
- package/dist/vite/keyframes.d.ts +129 -0
- package/dist/vite/keyframes.d.ts.map +1 -0
- package/dist/vite/keyframes.js +194 -0
- package/dist/vite/layout-editor.d.ts +45 -0
- package/dist/vite/layout-editor.d.ts.map +1 -0
- package/dist/vite/layout-editor.js +587 -0
- package/dist/vite/layout-transition.d.ts +136 -0
- package/dist/vite/layout-transition.d.ts.map +1 -0
- package/dist/vite/layout-transition.js +581 -0
- package/dist/vite/liquid-gradient-shader.d.ts +62 -0
- package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
- package/dist/vite/liquid-gradient-shader.js +413 -0
- package/dist/vite/mdx-client.d.ts +15 -0
- package/dist/vite/mdx-client.d.ts.map +1 -0
- package/dist/vite/mdx-client.js +265 -0
- package/dist/vite/mdx-parse.d.ts +102 -0
- package/dist/vite/mdx-parse.d.ts.map +1 -0
- package/dist/vite/mdx-parse.js +251 -0
- package/dist/vite/mdx-provided-components.d.ts +18 -0
- package/dist/vite/mdx-provided-components.d.ts.map +1 -0
- package/dist/vite/mdx-provided-components.js +14 -0
- package/dist/vite/mdx-video.d.ts +327 -0
- package/dist/vite/mdx-video.d.ts.map +1 -0
- package/dist/vite/mdx-video.js +596 -0
- package/dist/vite/mdx-video.test.d.ts +9 -0
- package/dist/vite/mdx-video.test.d.ts.map +1 -0
- package/dist/vite/mdx-video.test.js +1945 -0
- package/dist/vite/media-components.d.ts +45 -0
- package/dist/vite/media-components.d.ts.map +1 -0
- package/dist/vite/media-components.js +355 -0
- package/dist/vite/media-duration-store.d.ts +66 -0
- package/dist/vite/media-duration-store.d.ts.map +1 -0
- package/dist/vite/media-duration-store.js +204 -0
- package/dist/vite/player-page.d.ts +35 -0
- package/dist/vite/player-page.d.ts.map +1 -0
- package/dist/vite/player-page.js +720 -0
- package/dist/vite/render-client.d.ts +18 -0
- package/dist/vite/render-client.d.ts.map +1 -0
- package/dist/vite/render-client.js +67 -0
- package/dist/vite/sdk.d.ts +221 -0
- package/dist/vite/sdk.d.ts.map +1 -0
- package/dist/vite/sdk.js +355 -0
- package/dist/vite/server-components.d.ts +55 -0
- package/dist/vite/server-components.d.ts.map +1 -0
- package/dist/vite/server-components.js +156 -0
- package/dist/vite/server-mdx.d.ts +74 -0
- package/dist/vite/server-mdx.d.ts.map +1 -0
- package/dist/vite/server-mdx.js +259 -0
- package/dist/vite/shader-renderer.d.ts +48 -0
- package/dist/vite/shader-renderer.d.ts.map +1 -0
- package/dist/vite/shader-renderer.js +411 -0
- package/dist/vite/store-hooks.d.ts +12 -0
- package/dist/vite/store-hooks.d.ts.map +1 -0
- package/dist/vite/store-hooks.js +93 -0
- package/dist/vite/store.d.ts +75 -0
- package/dist/vite/store.d.ts.map +1 -0
- package/dist/vite/store.js +52 -0
- package/dist/vite/store.test.d.ts +6 -0
- package/dist/vite/store.test.d.ts.map +1 -0
- package/dist/vite/store.test.js +172 -0
- package/dist/vite/tweakpane-hook.d.ts +94 -0
- package/dist/vite/tweakpane-hook.d.ts.map +1 -0
- package/dist/vite/tweakpane-hook.js +450 -0
- package/dist/vite/vite-plugin.d.ts +28 -0
- package/dist/vite/vite-plugin.d.ts.map +1 -0
- package/dist/vite/vite-plugin.js +538 -0
- package/dist/vite/wave-gradient-shader.d.ts +41 -0
- package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
- package/dist/vite/wave-gradient-shader.js +207 -0
- package/package.json +101 -24
- package/skills/egaki/SKILL.md +162 -1
- package/src/cli/cache-utils.ts +169 -0
- package/src/cli/cached-generate.test.ts +531 -0
- package/src/cli/cached-generate.ts +348 -0
- package/src/cli/cartesia-provider.ts +249 -0
- package/src/{cli.ts → cli/cli.ts} +836 -884
- package/src/cli/clone-generate.ts +202 -0
- package/src/{credentials.ts → cli/credentials.ts} +20 -0
- package/src/cli/elevenlabs-speech-provider.ts +243 -0
- package/src/cli/generate.test.ts +166 -0
- package/src/cli/generate.ts +912 -0
- package/src/cli/main.ts +8 -0
- package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
- package/src/{models.ts → cli/models.ts} +65 -86
- package/src/cli/openai-speech-provider.ts +34 -0
- package/src/cli/separation-generate.ts +187 -0
- package/src/cli/speech-catalog.ts +253 -0
- package/src/cli/speech-generate.ts +275 -0
- package/src/cli/speech-models.ts +146 -0
- package/src/cli/speech-provider.test.ts +226 -0
- package/src/cli/transcription-catalog.ts +250 -0
- package/src/cli/transcription-generate.ts +214 -0
- package/src/cli/transcription-models.ts +136 -0
- package/src/cli/transcription-providers.ts +427 -0
- package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
- package/src/vite/angled-screen.tsx +214 -0
- package/src/vite/app.tsx +197 -0
- package/src/vite/bands-shader.tsx +319 -0
- package/src/vite/code-block.tsx +939 -0
- package/src/vite/components.tsx +278 -0
- package/src/vite/dispersion-rings-shader.tsx +331 -0
- package/src/vite/easing-curves.test.ts +232 -0
- package/src/vite/easing-curves.ts +743 -0
- package/src/vite/generated-media-client.tsx +121 -0
- package/src/vite/keyframes.tsx +347 -0
- package/src/vite/layout-editor.tsx +709 -0
- package/src/vite/layout-transition.tsx +706 -0
- package/src/vite/liquid-gradient-shader.tsx +474 -0
- package/src/vite/mdx-client.tsx +420 -0
- package/src/vite/mdx-parse.ts +343 -0
- package/src/vite/mdx-provided-components.ts +19 -0
- package/src/vite/mdx-video.test.tsx +2219 -0
- package/src/vite/mdx-video.tsx +976 -0
- package/src/vite/media-components.tsx +473 -0
- package/src/vite/media-duration-store.ts +225 -0
- package/src/vite/motion-timing.ts +194 -0
- package/src/vite/player-page.tsx +1107 -0
- package/src/vite/render-client.ts +84 -0
- package/src/vite/sdk.ts +598 -0
- package/src/vite/server-components.tsx +241 -0
- package/src/vite/server-mdx.ts +285 -0
- package/src/vite/shader-renderer.tsx +535 -0
- package/src/vite/store-hooks.ts +117 -0
- package/src/vite/store.test.ts +202 -0
- package/src/vite/store.ts +115 -0
- package/src/vite/styles.css +21 -0
- package/src/vite/tweakpane-hook.tsx +593 -0
- package/src/vite/virtual-modules.d.ts +30 -0
- package/src/vite/vite-plugin.ts +584 -0
- package/src/vite/wave-gradient-shader.tsx +253 -0
- package/README.md +0 -370
- package/dist/antigravity-auth.d.ts +0 -15
- package/dist/antigravity-auth.d.ts.map +0 -1
- package/dist/antigravity-auth.js +0 -544
- package/dist/antigravity-image.d.ts +0 -11
- package/dist/antigravity-image.d.ts.map +0 -1
- package/dist/antigravity-image.js +0 -285
- package/dist/chatgpt-auth.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.d.ts +0 -11
- package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.js +0 -64
- package/dist/login.d.ts.map +0 -1
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/models.d.ts.map +0 -1
- package/dist/open-browser.d.ts.map +0 -1
- package/dist/subscription.d.ts.map +0 -1
- package/dist/video-model-catalog.d.ts.map +0 -1
- package/dist/xai-auth.d.ts.map +0 -1
- /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
- /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
- /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
- /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
- /package/dist/{login.js → cli/login.js} +0 -0
- /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
- /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
- /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
- /package/dist/{subscription.js → cli/subscription.js} +0 -0
- /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
- /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
- /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
- /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
- /package/src/{login.ts → cli/login.ts} +0 -0
- /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
- /package/src/{subscription.ts → cli/subscription.ts} +0 -0
- /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 '
|
|
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
|