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.
- package/CHANGELOG.md +188 -0
- 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/cli.js +1329 -0
- 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/{credentials.d.ts → cli/credentials.d.ts} +11 -2
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/{credentials.js → cli/credentials.js} +88 -5
- 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/{login.js → cli/login.js} +45 -4
- 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/cli/model-catalog.d.ts +141 -0
- package/dist/cli/model-catalog.d.ts.map +1 -0
- package/dist/cli/model-catalog.js +2135 -0
- package/dist/{models.d.ts → cli/models.d.ts} +16 -9
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/cli/models.js +313 -0
- 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 +2 -0
- package/dist/cli/video-model-catalog.d.ts.map +1 -0
- package/dist/cli/video-model-catalog.js +5 -0
- package/dist/cli/xai-auth.d.ts +29 -0
- package/dist/cli/xai-auth.d.ts.map +1 -0
- package/dist/cli/xai-auth.js +325 -0
- 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 +103 -25
- 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/cli.ts +1818 -0
- package/src/cli/clone-generate.ts +202 -0
- package/src/{credentials.ts → cli/credentials.ts} +90 -7
- 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/{login.ts → cli/login.ts} +48 -3
- package/src/cli/main.ts +8 -0
- package/src/cli/model-catalog.ts +2336 -0
- package/src/cli/models.ts +408 -0
- 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/cli/video-model-catalog.ts +14 -0
- package/src/cli/xai-auth.ts +396 -0
- 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 -266
- 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/cli.js +0 -968
- 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 +0 -42
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/model-catalog.js +0 -1001
- package/dist/models.d.ts.map +0 -1
- package/dist/models.js +0 -246
- package/dist/open-browser.d.ts.map +0 -1
- package/dist/subscription.d.ts.map +0 -1
- package/dist/video-model-catalog.d.ts +0 -52
- package/dist/video-model-catalog.d.ts.map +0 -1
- package/dist/video-model-catalog.js +0 -306
- package/src/cli.ts +0 -1378
- package/src/model-catalog.ts +0 -1077
- package/src/models.ts +0 -309
- package/src/video-model-catalog.ts +0 -363
- /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/{login.d.ts → cli/login.d.ts} +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/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
- /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
- /package/src/{subscription.ts → cli/subscription.ts} +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// General-purpose persisted memoization for expensive async functions.
|
|
2
|
+
//
|
|
3
|
+
// cachedGenerate() wraps any async function with:
|
|
4
|
+
// - Deterministic filesystem caching (params → stable hash → file)
|
|
5
|
+
// - Concurrent call deduplication (same key → same promise)
|
|
6
|
+
// - Stale fallback management (old results moved to stale/ subfolder)
|
|
7
|
+
// - Progress tracking (namespace-based, drives the player toolbar UI)
|
|
8
|
+
//
|
|
9
|
+
// Binary params (Uint8Array) are auto-hashed in the cache key. The caller
|
|
10
|
+
// only provides a generate function, a serializer, and a namespace.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// const generateImage = cachedGenerate({
|
|
14
|
+
// namespace: 'image',
|
|
15
|
+
// prefixFrom: (p) => p.prompt,
|
|
16
|
+
// generate: (p) => generateImageUncached(p).then(r => r.images[0]!),
|
|
17
|
+
// serialize: (file) => ({ bytes: file.uint8Array, extension: '.png' }),
|
|
18
|
+
// })
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
import {
|
|
23
|
+
stableJsonKey, hashKey, promptPrefix,
|
|
24
|
+
generatedDir, findCachedFile, findFallbackFile, moveFileToStale,
|
|
25
|
+
generationQueue, formatKeyParams,
|
|
26
|
+
} from './cache-utils.js'
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Progress registry — tracks all active generations with metadata so the
|
|
30
|
+
// client and agents can observe progress across all scenes.
|
|
31
|
+
// Keyed by namespace string (not a fixed enum), so any user-defined
|
|
32
|
+
// namespace automatically gets tracking.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface GenerationEntry {
|
|
36
|
+
namespace: string
|
|
37
|
+
label: string
|
|
38
|
+
model?: string
|
|
39
|
+
startedAt: number
|
|
40
|
+
params: Record<string, unknown>
|
|
41
|
+
/** URL path to the in-progress file. Set by onPartial writes during streaming
|
|
42
|
+
* generation (e.g. accumulated audio written every ~2s). Cleared when
|
|
43
|
+
* generation completes. Flows through the progress SSE to the client. */
|
|
44
|
+
partialSrc?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const generationRegistry: Map<string, GenerationEntry> =
|
|
48
|
+
(globalThis as any).__egakiGenerationRegistry ??= new Map<string, GenerationEntry>()
|
|
49
|
+
|
|
50
|
+
type ProgressListener = () => void
|
|
51
|
+
const progressListeners: Set<ProgressListener> =
|
|
52
|
+
(globalThis as any).__egakiProgressListeners ??= new Set<ProgressListener>()
|
|
53
|
+
|
|
54
|
+
function notifyProgressListeners() {
|
|
55
|
+
for (const listener of progressListeners) {
|
|
56
|
+
try { listener() } catch { /* listener errors are non-fatal */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Subscribe to generation progress changes. Returns an unsubscribe function. */
|
|
61
|
+
export function onProgressChange(listener: ProgressListener): () => void {
|
|
62
|
+
progressListeners.add(listener)
|
|
63
|
+
return () => { progressListeners.delete(listener) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GenerationProgressSummary {
|
|
67
|
+
counts: Record<string, number>
|
|
68
|
+
total: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface GenerationProgressEntry extends GenerationEntry {
|
|
72
|
+
key: string
|
|
73
|
+
elapsedMs: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface GenerationError {
|
|
77
|
+
key: string
|
|
78
|
+
namespace: string
|
|
79
|
+
label: string
|
|
80
|
+
model?: string
|
|
81
|
+
error: string
|
|
82
|
+
durationMs: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface GenerationProgressEvent {
|
|
86
|
+
generations: GenerationProgressEntry[]
|
|
87
|
+
errors: GenerationError[]
|
|
88
|
+
summary: GenerationProgressSummary
|
|
89
|
+
done: boolean
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Recent generation errors, drained on each getGenerationProgress() call
|
|
93
|
+
* so each SSE event includes errors that happened since the last yield. */
|
|
94
|
+
const recentErrors: GenerationError[] =
|
|
95
|
+
(globalThis as any).__egakiRecentErrors ??= [] as GenerationError[]
|
|
96
|
+
|
|
97
|
+
/** Get a snapshot of all active generations with elapsed time computed.
|
|
98
|
+
* Drains the error queue so each call returns only new errors since last read. */
|
|
99
|
+
export function getGenerationProgress(): GenerationProgressEvent {
|
|
100
|
+
const now = Date.now()
|
|
101
|
+
const generations: GenerationProgressEntry[] = []
|
|
102
|
+
const counts: Record<string, number> = {}
|
|
103
|
+
for (const [key, entry] of generationRegistry) {
|
|
104
|
+
generations.push({ ...entry, key, elapsedMs: now - entry.startedAt })
|
|
105
|
+
counts[entry.namespace] = (counts[entry.namespace] ?? 0) + 1
|
|
106
|
+
}
|
|
107
|
+
const total = generations.length
|
|
108
|
+
const errors = recentErrors.splice(0)
|
|
109
|
+
return {
|
|
110
|
+
generations,
|
|
111
|
+
errors,
|
|
112
|
+
summary: { counts, total },
|
|
113
|
+
done: total === 0 && errors.length === 0,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function registerGeneration(key: string, entry: GenerationEntry) {
|
|
118
|
+
generationRegistry.set(key, entry)
|
|
119
|
+
notifyProgressListeners()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function unregisterGeneration(key: string, error?: string) {
|
|
123
|
+
const entry = generationRegistry.get(key)
|
|
124
|
+
if (entry && error) {
|
|
125
|
+
recentErrors.push({
|
|
126
|
+
key,
|
|
127
|
+
namespace: entry.namespace,
|
|
128
|
+
label: entry.label,
|
|
129
|
+
model: entry.model,
|
|
130
|
+
error,
|
|
131
|
+
durationMs: Date.now() - entry.startedAt,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
generationRegistry.delete(key)
|
|
135
|
+
notifyProgressListeners()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Auto cache key — recursively walks params, hashes Uint8Array values,
|
|
140
|
+
// strips undefined. Handles nested objects and arrays of any depth.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function normalizeCacheValue(value: unknown): unknown {
|
|
144
|
+
if (value === undefined) return undefined
|
|
145
|
+
if (value instanceof Uint8Array) return hashKey(value)
|
|
146
|
+
if (Array.isArray(value)) return value.map(normalizeCacheValue)
|
|
147
|
+
if (value && typeof value === 'object') {
|
|
148
|
+
return Object.fromEntries(
|
|
149
|
+
Object.entries(value as Record<string, unknown>)
|
|
150
|
+
.filter(([, v]) => v !== undefined)
|
|
151
|
+
.map(([k, v]) => [k, normalizeCacheValue(v)]),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
return value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function autoCacheKey(params: Record<string, unknown>): Record<string, unknown> {
|
|
158
|
+
return normalizeCacheValue(params) as Record<string, unknown>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// cachedGenerate — the higher-order function
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
type SerializeResult =
|
|
166
|
+
| { bytes: Uint8Array; extension: string; prefix?: string }
|
|
167
|
+
| { json: unknown; extension: '.json'; prefix?: string }
|
|
168
|
+
|
|
169
|
+
export interface CachedGenerateConfig<TParams, TGenerated, TResult> {
|
|
170
|
+
/** Filesystem namespace: files go to `public/generated/{namespace}/` */
|
|
171
|
+
namespace: string
|
|
172
|
+
|
|
173
|
+
/** Human-readable filename prefix derived from params */
|
|
174
|
+
prefixFrom: (params: TParams) => string
|
|
175
|
+
|
|
176
|
+
/** Extract the cache-relevant subset of params. Use this to exclude fields
|
|
177
|
+
* that affect naming but not identity (e.g. `filename` in transcription).
|
|
178
|
+
* Defaults to using all params. Uint8Array values are auto-hashed regardless. */
|
|
179
|
+
cacheKey?: (params: TParams) => Record<string, unknown>
|
|
180
|
+
|
|
181
|
+
/** The actual generation function. Throw on error. */
|
|
182
|
+
generate: (params: TParams) => Promise<TGenerated>
|
|
183
|
+
|
|
184
|
+
/** How to persist the result to disk.
|
|
185
|
+
* Return { bytes, extension } for binary files or { json, extension: '.json' } for data.
|
|
186
|
+
* Optionally return { prefix } to override the filename prefix (e.g. when
|
|
187
|
+
* the readable name comes from the result, not the params). */
|
|
188
|
+
serialize: (result: TGenerated, params: TParams) => SerializeResult
|
|
189
|
+
|
|
190
|
+
/** How to read a cached result back.
|
|
191
|
+
* For binary: receives { urlPath, filePath }, defaults to { src: urlPath }.
|
|
192
|
+
* For JSON: filePath contains the JSON data. */
|
|
193
|
+
deserialize?: (cached: { urlPath: string; filePath: string }) => TResult
|
|
194
|
+
|
|
195
|
+
/** Model ID key in params, used for progress tracking display. */
|
|
196
|
+
modelFrom?: (params: TParams) => string | undefined
|
|
197
|
+
|
|
198
|
+
/** Called after the main file is written to disk. Use this to write
|
|
199
|
+
* sidecar files (e.g. timestamps JSON alongside audio).
|
|
200
|
+
* Receives the generated result so it can extract extra data to persist. */
|
|
201
|
+
postWrite?: (filePath: string, result: TGenerated, params: TParams) => void
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Result of getCacheInfo — used by server components for sync cache checks
|
|
205
|
+
* and fallback lookup before starting async generation. */
|
|
206
|
+
export interface CacheInfo {
|
|
207
|
+
hash: string
|
|
208
|
+
prefix: string
|
|
209
|
+
dir: string
|
|
210
|
+
key: string
|
|
211
|
+
cached: string | undefined
|
|
212
|
+
fallback: { filename: string; inStale: boolean } | undefined
|
|
213
|
+
fallbackSrc: string | undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Compute cache info for a given namespace and params without starting generation.
|
|
217
|
+
* Used by server components to check cache and find fallback synchronously. */
|
|
218
|
+
export function getCacheInfo(
|
|
219
|
+
namespace: string,
|
|
220
|
+
params: Record<string, unknown>,
|
|
221
|
+
prefixText: string,
|
|
222
|
+
): CacheInfo {
|
|
223
|
+
const keyParams = autoCacheKey(params)
|
|
224
|
+
// Note: getCacheInfo uses the raw params. If a cachedGenerate wrapper uses
|
|
225
|
+
// cacheKey to exclude fields, the caller should pass the same subset here.
|
|
226
|
+
const key = stableJsonKey(keyParams)
|
|
227
|
+
const hash = hashKey(key)
|
|
228
|
+
const dir = generatedDir(namespace)
|
|
229
|
+
const prefix = promptPrefix(prefixText)
|
|
230
|
+
|
|
231
|
+
const cached = findCachedFile(dir, hash)
|
|
232
|
+
const fallback = cached ? undefined : findFallbackFile(dir, prefix, hash)
|
|
233
|
+
const fallbackSrc = fallback
|
|
234
|
+
? `/generated/${namespace}/${fallback.inStale ? 'stale/' : ''}${fallback.filename}`
|
|
235
|
+
: undefined
|
|
236
|
+
|
|
237
|
+
return { hash, prefix, dir, key, cached, fallback, fallbackSrc }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type CachedGenerateFn<TParams, TResult> = {
|
|
241
|
+
(params: TParams): Promise<Error | TResult>
|
|
242
|
+
/** Sync cache check using the same config as the cached function. */
|
|
243
|
+
getCacheInfo(params: TParams): CacheInfo
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function cachedGenerate<TParams, TGenerated, TResult = { src: string }>(
|
|
247
|
+
config: CachedGenerateConfig<TParams, TGenerated, TResult>,
|
|
248
|
+
): CachedGenerateFn<TParams, TResult> {
|
|
249
|
+
const { namespace, prefixFrom, cacheKey, generate, serialize, deserialize, modelFrom, postWrite } = config
|
|
250
|
+
|
|
251
|
+
function cachedFn(params: TParams): Promise<Error | TResult> {
|
|
252
|
+
const rawKeyParams = cacheKey ? cacheKey(params) : (params as Record<string, unknown>)
|
|
253
|
+
const keyParams = autoCacheKey(rawKeyParams)
|
|
254
|
+
const key = stableJsonKey(keyParams)
|
|
255
|
+
const hash = hashKey(key)
|
|
256
|
+
const dir = generatedDir(namespace)
|
|
257
|
+
const prefix = promptPrefix(prefixFrom(params))
|
|
258
|
+
|
|
259
|
+
// Queue key includes namespace to prevent cross-namespace collisions.
|
|
260
|
+
// Two wrappers with different namespaces but identical params must not
|
|
261
|
+
// share the same in-flight promise.
|
|
262
|
+
const queueKey = `${namespace}:${key}`
|
|
263
|
+
|
|
264
|
+
// 1. Cache hit — return immediately
|
|
265
|
+
const cached = findCachedFile(dir, hash)
|
|
266
|
+
if (cached) {
|
|
267
|
+
const urlPath = `/generated/${namespace}/${cached}`
|
|
268
|
+
const filePath = path.join(dir, cached)
|
|
269
|
+
if (deserialize) {
|
|
270
|
+
return Promise.resolve(deserialize({ urlPath, filePath }))
|
|
271
|
+
}
|
|
272
|
+
return Promise.resolve({ src: urlPath } as TResult)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 2. Dedup — return pending promise if same key is in-flight
|
|
276
|
+
const pending = generationQueue.get(queueKey)
|
|
277
|
+
if (pending) return pending
|
|
278
|
+
|
|
279
|
+
// 3. Find stale fallback for cleanup after generation
|
|
280
|
+
const fallback = findFallbackFile(dir, prefix, hash)
|
|
281
|
+
|
|
282
|
+
// 4. Register progress
|
|
283
|
+
const regKey = `${namespace}:${hash}`
|
|
284
|
+
const label = prefixFrom(params)
|
|
285
|
+
registerGeneration(regKey, {
|
|
286
|
+
namespace,
|
|
287
|
+
label,
|
|
288
|
+
model: modelFrom?.(params),
|
|
289
|
+
startedAt: Date.now(),
|
|
290
|
+
params: keyParams,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// 5. Generate, serialize, write to disk
|
|
294
|
+
// The entire body is wrapped in try/catch so that failures in serialize(),
|
|
295
|
+
// fs.writeFileSync(), or deserialize() also unregister progress and return
|
|
296
|
+
// Error instead of throwing through the promise.
|
|
297
|
+
const promise = (async (): Promise<Error | TResult> => {
|
|
298
|
+
try {
|
|
299
|
+
console.log(`[egaki] generating ${namespace}: ${prefix}-${hash} (${formatKeyParams(keyParams)})`)
|
|
300
|
+
|
|
301
|
+
const result = await generate(params)
|
|
302
|
+
|
|
303
|
+
const serialized = serialize(result, params)
|
|
304
|
+
const finalPrefix = serialized.prefix ? promptPrefix(serialized.prefix) : prefix
|
|
305
|
+
const filename = `${finalPrefix}-${hash}${serialized.extension}`
|
|
306
|
+
const filePath = path.join(dir, filename)
|
|
307
|
+
|
|
308
|
+
if ('bytes' in serialized) {
|
|
309
|
+
fs.writeFileSync(filePath, serialized.bytes)
|
|
310
|
+
} else {
|
|
311
|
+
fs.writeFileSync(filePath, JSON.stringify(serialized.json, null, 2))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (postWrite) {
|
|
315
|
+
try { postWrite(filePath, result, params) } catch (e) {
|
|
316
|
+
console.error(`[egaki] postWrite error for ${namespace}:`, e)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`[egaki] generated ${namespace}: ${filename}`)
|
|
321
|
+
if (fallback) moveFileToStale(dir, fallback)
|
|
322
|
+
unregisterGeneration(regKey)
|
|
323
|
+
|
|
324
|
+
const urlPath = `/generated/${namespace}/${filename}`
|
|
325
|
+
if (deserialize) {
|
|
326
|
+
return deserialize({ urlPath, filePath })
|
|
327
|
+
}
|
|
328
|
+
return { src: urlPath } as TResult
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
331
|
+
unregisterGeneration(regKey, error.message)
|
|
332
|
+
return error
|
|
333
|
+
} finally {
|
|
334
|
+
generationQueue.delete(queueKey)
|
|
335
|
+
}
|
|
336
|
+
})()
|
|
337
|
+
|
|
338
|
+
generationQueue.set(queueKey, promise)
|
|
339
|
+
return promise
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
cachedFn.getCacheInfo = function (params: TParams): CacheInfo {
|
|
343
|
+
const rawKeyParams = cacheKey ? cacheKey(params) : (params as Record<string, unknown>)
|
|
344
|
+
return getCacheInfo(namespace, rawKeyParams, prefixFrom(params))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return cachedFn
|
|
348
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Custom Cartesia TTS and STT providers.
|
|
2
|
+
//
|
|
3
|
+
// TTS: POST https://api.cartesia.ai/tts/sse with add_timestamps=true
|
|
4
|
+
// Returns word-level timestamps natively via SSE events.
|
|
5
|
+
// API docs: https://docs.cartesia.ai/api-reference/tts/sse
|
|
6
|
+
//
|
|
7
|
+
// STT: POST https://api.cartesia.ai/stt (Ink models)
|
|
8
|
+
// API docs: https://docs.cartesia.ai/api-reference/stt/transcribe
|
|
9
|
+
//
|
|
10
|
+
// Source of truth for request/response types:
|
|
11
|
+
// JS SDK (auto-generated from OpenAPI spec by Stainless):
|
|
12
|
+
// https://github.com/cartesia-ai/cartesia-js/blob/main/src/resources/stt/stt.ts
|
|
13
|
+
// https://github.com/cartesia-ai/cartesia-js/blob/main/src/resources/tts.ts
|
|
14
|
+
//
|
|
15
|
+
// TTS models: https://docs.cartesia.ai/build-with-cartesia/tts-models/latest
|
|
16
|
+
// STT models: https://docs.cartesia.ai/build-with-cartesia/stt/latest
|
|
17
|
+
// Pricing: https://docs.cartesia.ai/pricing
|
|
18
|
+
|
|
19
|
+
import type { SpeechProvider, SpeechProviderOptions, SpeechProviderResult } from './speech-generate.js'
|
|
20
|
+
import type { WordTimestamp } from './transcription-generate.js'
|
|
21
|
+
|
|
22
|
+
const CARTESIA_API_BASE = 'https://api.cartesia.ai'
|
|
23
|
+
const CARTESIA_API_VERSION = '2026-03-01'
|
|
24
|
+
|
|
25
|
+
// Default voice: Katie (en-US Female), recommended for voice agents
|
|
26
|
+
const DEFAULT_VOICE_ID = 'f786b574-daa5-4673-aa0c-cbe3e8534c02'
|
|
27
|
+
|
|
28
|
+
// ─── shared helpers ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map user-facing format strings to Cartesia output_format objects.
|
|
32
|
+
* The SSE endpoint only supports raw container (not mp3/wav), so we
|
|
33
|
+
* always use raw PCM for SSE streaming.
|
|
34
|
+
*/
|
|
35
|
+
function buildSseOutputFormat(format?: string): {
|
|
36
|
+
container: 'raw'
|
|
37
|
+
encoding: string
|
|
38
|
+
sample_rate: number
|
|
39
|
+
} {
|
|
40
|
+
// SSE endpoint only supports raw container. We always use PCM s16le
|
|
41
|
+
// and let the caller convert if needed.
|
|
42
|
+
const sampleRate = format === 'pcm_16000' ? 16000
|
|
43
|
+
: format === 'pcm_22050' ? 22050
|
|
44
|
+
: format === 'pcm_24000' ? 24000
|
|
45
|
+
: 44100
|
|
46
|
+
return { container: 'raw', encoding: 'pcm_s16le', sample_rate: sampleRate }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── SSE event types ─────────────────────────────────────────────────────────
|
|
50
|
+
// From https://docs.cartesia.ai/api-reference/tts/sse
|
|
51
|
+
|
|
52
|
+
interface CartesiaSseChunk {
|
|
53
|
+
type: 'chunk'
|
|
54
|
+
data: string // base64 encoded audio
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface CartesiaSseTimestamps {
|
|
58
|
+
type: 'timestamps'
|
|
59
|
+
word_timestamps: {
|
|
60
|
+
words: string[]
|
|
61
|
+
start: number[]
|
|
62
|
+
end: number[]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface CartesiaSseDone {
|
|
67
|
+
type: 'done'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface CartesiaSseError {
|
|
71
|
+
type: 'error'
|
|
72
|
+
message: string
|
|
73
|
+
status_code: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type CartesiaSseEvent = CartesiaSseChunk | CartesiaSseTimestamps | CartesiaSseDone | CartesiaSseError
|
|
77
|
+
|
|
78
|
+
// ─── SSE parser ──────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
import { EventSourceParserStream } from 'eventsource-parser/stream'
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a Cartesia SSE stream into audio chunks and word timestamps.
|
|
84
|
+
* Uses eventsource-parser for robust SSE frame parsing.
|
|
85
|
+
*/
|
|
86
|
+
export async function parseCartesiaSseStream(
|
|
87
|
+
body: ReadableStream<Uint8Array>,
|
|
88
|
+
): Promise<{ audioChunks: Uint8Array[]; timestamps: WordTimestamp[] }> {
|
|
89
|
+
const audioChunks: Uint8Array[] = []
|
|
90
|
+
const timestamps: WordTimestamp[] = []
|
|
91
|
+
|
|
92
|
+
const eventStream = body
|
|
93
|
+
.pipeThrough(new TextDecoderStream() as any)
|
|
94
|
+
.pipeThrough(new EventSourceParserStream())
|
|
95
|
+
|
|
96
|
+
const reader = eventStream.getReader()
|
|
97
|
+
|
|
98
|
+
while (true) {
|
|
99
|
+
const { done, value } = await reader.read()
|
|
100
|
+
if (done) break
|
|
101
|
+
|
|
102
|
+
let event: CartesiaSseEvent
|
|
103
|
+
try {
|
|
104
|
+
event = JSON.parse(value.data)
|
|
105
|
+
} catch {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (event.type === 'chunk') {
|
|
110
|
+
audioChunks.push(new Uint8Array(Buffer.from(event.data, 'base64')))
|
|
111
|
+
} else if (event.type === 'timestamps') {
|
|
112
|
+
const ts = event.word_timestamps
|
|
113
|
+
for (let i = 0; i < ts.words.length; i++) {
|
|
114
|
+
timestamps.push({
|
|
115
|
+
word: ts.words[i]!,
|
|
116
|
+
startSecond: ts.start[i]!,
|
|
117
|
+
endSecond: ts.end[i]!,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
} else if (event.type === 'error') {
|
|
121
|
+
throw new Error(`Cartesia TTS SSE error ${event.status_code}: ${event.message}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { audioChunks, timestamps }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Concatenate multiple Uint8Array chunks into one. */
|
|
129
|
+
function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {
|
|
130
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0)
|
|
131
|
+
const result = new Uint8Array(totalLength)
|
|
132
|
+
let offset = 0
|
|
133
|
+
for (const chunk of chunks) {
|
|
134
|
+
result.set(chunk, offset)
|
|
135
|
+
offset += chunk.length
|
|
136
|
+
}
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Wrap raw PCM s16le mono audio into a WAV container.
|
|
142
|
+
* The SSE endpoint only supports raw PCM output, but browsers need a
|
|
143
|
+
* proper container format. WAV is the simplest lossless wrapper.
|
|
144
|
+
*/
|
|
145
|
+
function wavFromPcmS16Le(pcm: Uint8Array, sampleRate: number): Uint8Array {
|
|
146
|
+
const header = new ArrayBuffer(44)
|
|
147
|
+
const view = new DataView(header)
|
|
148
|
+
const channels = 1
|
|
149
|
+
const bitsPerSample = 16
|
|
150
|
+
const byteRate = sampleRate * channels * (bitsPerSample / 8)
|
|
151
|
+
const blockAlign = channels * (bitsPerSample / 8)
|
|
152
|
+
|
|
153
|
+
const writeStr = (offset: number, str: string) => {
|
|
154
|
+
for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeStr(0, 'RIFF')
|
|
158
|
+
view.setUint32(4, 36 + pcm.byteLength, true)
|
|
159
|
+
writeStr(8, 'WAVE')
|
|
160
|
+
writeStr(12, 'fmt ')
|
|
161
|
+
view.setUint32(16, 16, true) // subchunk1 size (PCM)
|
|
162
|
+
view.setUint16(20, 1, true) // audio format (PCM = 1)
|
|
163
|
+
view.setUint16(22, channels, true)
|
|
164
|
+
view.setUint32(24, sampleRate, true)
|
|
165
|
+
view.setUint32(28, byteRate, true)
|
|
166
|
+
view.setUint16(32, blockAlign, true)
|
|
167
|
+
view.setUint16(34, bitsPerSample, true)
|
|
168
|
+
writeStr(36, 'data')
|
|
169
|
+
view.setUint32(40, pcm.byteLength, true)
|
|
170
|
+
|
|
171
|
+
const wav = new Uint8Array(44 + pcm.byteLength)
|
|
172
|
+
wav.set(new Uint8Array(header), 0)
|
|
173
|
+
wav.set(pcm, 44)
|
|
174
|
+
return wav
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Cartesia TTS SpeechProvider ─────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a Cartesia speech provider using the SSE endpoint with word timestamps.
|
|
181
|
+
* Returns raw PCM audio (pcm_s16le) since the SSE endpoint only supports raw container.
|
|
182
|
+
*/
|
|
183
|
+
export function createCartesiaSpeechProvider(): SpeechProvider {
|
|
184
|
+
return {
|
|
185
|
+
async generate(options: SpeechProviderOptions): Promise<SpeechProviderResult> {
|
|
186
|
+
const apiKey = process.env['CARTESIA_API_KEY']
|
|
187
|
+
if (!apiKey) {
|
|
188
|
+
throw new Error('Missing CARTESIA_API_KEY. Run: egaki login --provider cartesia --key <key>')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const voiceId = options.voice || DEFAULT_VOICE_ID
|
|
192
|
+
const outputFormat = buildSseOutputFormat(options.outputFormat)
|
|
193
|
+
|
|
194
|
+
const body: Record<string, unknown> = {
|
|
195
|
+
model_id: options.modelId,
|
|
196
|
+
transcript: options.text,
|
|
197
|
+
voice: { mode: 'id', id: voiceId },
|
|
198
|
+
output_format: outputFormat,
|
|
199
|
+
add_timestamps: true,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.language) {
|
|
203
|
+
body.language = options.language
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (options.speed != null) {
|
|
207
|
+
body.generation_config = { speed: options.speed }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const response = await fetch(`${CARTESIA_API_BASE}/tts/sse`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'Content-Type': 'application/json',
|
|
214
|
+
'Cartesia-Version': CARTESIA_API_VERSION,
|
|
215
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
216
|
+
},
|
|
217
|
+
body: JSON.stringify(body),
|
|
218
|
+
signal: options.abortSignal,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const errorText = await response.text().catch(() => '')
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Cartesia TTS SSE API error ${response.status}: ${errorText || response.statusText}`,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!response.body) {
|
|
229
|
+
throw new Error('Cartesia TTS SSE response has no body')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { audioChunks, timestamps } = await parseCartesiaSseStream(response.body)
|
|
233
|
+
const pcm = concatUint8Arrays(audioChunks)
|
|
234
|
+
// Wrap raw PCM in WAV container so browsers can play the audio.
|
|
235
|
+
const audio = wavFromPcmS16Le(pcm, outputFormat.sample_rate)
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
audio,
|
|
239
|
+
mediaType: 'audio/wav',
|
|
240
|
+
timestamps: timestamps.length > 0 ? timestamps : undefined,
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Cartesia STT is now implemented as a TranscriptionProvider in
|
|
247
|
+
// transcription-providers.ts (createCartesiaTranscriptionProvider).
|
|
248
|
+
// The old TranscriptionModel-based implementation was removed to drop
|
|
249
|
+
// the AI SDK dependency from the transcription path.
|