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,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.