egaki 0.6.0 → 0.7.1

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 +145 -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 +539 -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 +585 -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
@@ -36,6 +36,17 @@ export type { ImageModelEntry, VideoModelEntry, AnyModelEntry }
36
36
  /** @deprecated Use ImageModelEntry instead */
37
37
  export type ModelEntry = ImageModelEntry
38
38
 
39
+ /**
40
+ * Validation error for user-facing input issues (wrong model, missing key, etc.).
41
+ * CLI prints just the message; provider/runtime errors get full stack + details.
42
+ */
43
+ export class ValidationError extends Error {
44
+ constructor(message: string) {
45
+ super(message)
46
+ this.name = 'ValidationError'
47
+ }
48
+ }
49
+
39
50
  export const IMAGE_MODELS = CATALOG.map((m) => m.id) as [string, ...string[]]
40
51
  export const VIDEO_MODELS = VIDEO_CATALOG.map((m) => m.id) as [string, ...string[]]
41
52
 
@@ -52,11 +63,10 @@ function stripProviderPrefix(modelId: string): string {
52
63
  export const DEFAULT_MODEL = 'nano-banana-pro-preview'
53
64
  export const DEFAULT_VIDEO_MODEL = 'veo-3.1-fast-generate-001'
54
65
 
55
- export function getModelConfig(modelId: string): AnyModelEntry {
66
+ export function getModelConfig(modelId: string): Error | AnyModelEntry {
56
67
  const entry = findAnyModel(modelId)
57
68
  if (!entry) {
58
- console.error(pc.red(`Unknown model: ${modelId}`))
59
- process.exit(1)
69
+ return new ValidationError(`Unknown model: ${modelId}`)
60
70
  }
61
71
  return entry
62
72
  }
@@ -72,9 +82,9 @@ export function getModelConfig(modelId: string): AnyModelEntry {
72
82
  // That's it — no switch statements to touch.
73
83
 
74
84
  type ProviderSdk = {
75
- image?: (modelId: string) => Promise<ImageModel>
76
- text?: (modelId: string) => Promise<LanguageModel>
77
- video?: (modelId: string) => Promise<any>
85
+ image?: (modelId: string) => Promise<Error | ImageModel>
86
+ text?: (modelId: string) => Promise<Error | LanguageModel>
87
+ video?: (modelId: string) => Promise<Error | any>
78
88
  }
79
89
 
80
90
  // Vertex is the only provider that uses a prefix (vertex/model-id) in the catalog
@@ -105,10 +115,12 @@ const PROVIDER_SDKS: Record<string, ProviderSdk> = {
105
115
  xai: {
106
116
  image: async (id) => {
107
117
  const xaiProvider = await getXaiProviderInstance()
118
+ if (xaiProvider instanceof Error) return xaiProvider
108
119
  return xaiProvider.image(id)
109
120
  },
110
121
  video: async (id) => {
111
122
  const xaiProvider = await getXaiProviderInstance()
123
+ if (xaiProvider instanceof Error) return xaiProvider
112
124
  return xaiProvider.video(id)
113
125
  },
114
126
  },
@@ -122,7 +134,7 @@ const PROVIDER_SDKS: Record<string, ProviderSdk> = {
122
134
  // OAuth tokens from the Grok Build subscription. OAuth tokens are injected via
123
135
  // a custom fetch that replaces the Authorization header on every request.
124
136
 
125
- async function getXaiProviderInstance() {
137
+ async function getXaiProviderInstance(): Promise<Error | ReturnType<Awaited<typeof import('@ai-sdk/xai')>['createXai']>> {
126
138
  const { createXai } = await import('@ai-sdk/xai')
127
139
 
128
140
  // Direct API key path (env or stored)
@@ -135,14 +147,12 @@ async function getXaiProviderInstance() {
135
147
  // OAuth path: inject bearer token via custom fetch
136
148
  const storedAuth = getXaiAuth()
137
149
  if (!storedAuth) {
138
- console.error(pc.red('Missing xAI OAuth tokens. Please run `egaki login` and select xAI Grok Build.'))
139
- process.exit(1)
150
+ return new ValidationError('Missing xAI OAuth tokens. Please run `egaki login` and select xAI Grok Build.')
140
151
  }
141
152
 
142
153
  const auth = await getValidXaiAuth(storedAuth, saveXaiAuth)
143
154
  if (auth instanceof Error) {
144
- console.error(pc.red(auth.message))
145
- process.exit(1)
155
+ return auth
146
156
  }
147
157
 
148
158
  return createXai({
@@ -179,54 +189,29 @@ function hasDirectProviderKey(providerName: string): boolean {
179
189
  }
180
190
 
181
191
  // Check that the provider's API key or OAuth login is available before making API calls.
182
- // Prints a user-friendly error with instructions on how to configure it.
183
- export function ensureProviderKey(providerName: string): void {
184
- if (hasDirectProviderKey(providerName)) return
192
+ // Returns an Error with a user-friendly message if no key is found.
193
+ export function ensureProviderKey(providerName: string): Error | undefined {
194
+ if (hasDirectProviderKey(providerName)) return undefined
185
195
 
186
196
  // Vertex models require a direct key — the upstream Vercel AI Gateway
187
197
  // does not support vertex/ routing, so egaki gateway can't proxy them.
188
198
  if (providerName === 'vertex') {
189
- console.error('')
190
- console.error(pc.red(pc.bold('Vertex models require a direct Google Cloud API key')))
191
- console.error('')
192
- console.error(` ${pc.cyan('egaki login --provider vertex --key <key>')}`)
193
- console.error('')
194
- console.error(pc.dim('Get a key at https://console.cloud.google.com/apis/credentials'))
195
- console.error(pc.dim('Egaki subscription does not cover Vertex routing yet.'))
196
- console.error('')
197
- process.exit(1)
199
+ return new ValidationError(
200
+ 'Vertex models require a direct Google Cloud API key. ' +
201
+ 'Run: egaki login --provider vertex --key <key>. ' +
202
+ 'Get a key at https://console.cloud.google.com/apis/credentials',
203
+ )
198
204
  }
199
205
 
200
- if (hasEgakiKey()) return
206
+ if (hasEgakiKey()) return undefined
201
207
 
202
208
  const info = PROVIDERS[providerName]
203
-
204
- console.error('')
205
- console.error(pc.red(pc.bold(`Missing API key for ${info?.label || providerName}`)))
206
- console.error('')
207
- console.error(` ${pc.bold('Recommended:')} Use Egaki subscription (all models, one key)`)
208
- console.error('')
209
- console.error(
210
- ` ${pc.cyan('egaki subscribe')} get started in 30 seconds`,
211
- )
212
- console.error('')
213
- console.error(` ${pc.dim('Or configure a provider key directly:')}`)
214
- console.error('')
215
- console.error(
216
- ` ${pc.cyan('egaki login')} interactive setup`,
209
+ const label = info?.label || providerName
210
+ return new ValidationError(
211
+ `Missing API key for ${label}. ` +
212
+ 'Use an Egaki subscription (egaki subscribe) or configure a provider key directly (egaki login).' +
213
+ (info ? ` ${info.hint}` : ''),
217
214
  )
218
- if (info) {
219
- console.error(
220
- ` ${pc.cyan(`${info.envVar}=...`)} egaki image ... inline env var`,
221
- )
222
- console.error(
223
- ` ${pc.cyan(`egaki login --provider ${providerName} --key <key>`)}`,
224
- )
225
- console.error('')
226
- console.error(` ${pc.dim(info.hint)}`)
227
- }
228
- console.error('')
229
- process.exit(1)
230
215
  }
231
216
 
232
217
  // ─── gateway model factories ─────────────────────────────────────────────────
@@ -335,9 +320,12 @@ function logAuthSource(source: AuthSource): void {
335
320
  // Priority for others: direct provider key > egaki gateway > error.
336
321
  // Uses PROVIDER_SDKS map for direct provider creation, eliminating switch blocks.
337
322
 
338
- export async function createImageModel(modelId: string): Promise<ImageModel> {
323
+ export async function createImageModel(modelId: string): Promise<Error | ImageModel> {
339
324
  const config = getModelConfig(modelId)
340
- ensureProviderKey(config.provider)
325
+ if (config instanceof Error) return config
326
+
327
+ const keyError = ensureProviderKey(config.provider)
328
+ if (keyError) return keyError
341
329
 
342
330
  const authSource = resolveAuthSource(config.provider)
343
331
  logAuthSource(authSource)
@@ -345,31 +333,30 @@ export async function createImageModel(modelId: string): Promise<ImageModel> {
345
333
  if (hasDirectProviderKey(config.provider)) {
346
334
  const factory = PROVIDER_SDKS[config.provider]?.image
347
335
  if (factory) {
348
- return factory(modelId)
336
+ const result = await factory(modelId)
337
+ if (result instanceof Error) return result
338
+ return result
349
339
  }
350
- console.error(
351
- pc.red(
352
- `Direct image generation is not supported for provider: ${config.provider}.`,
353
- ),
354
- )
355
- process.exit(1)
340
+ return new ValidationError(`Direct image generation is not supported for provider: ${config.provider}.`)
356
341
  }
357
342
 
358
343
  if (hasEgakiKey()) {
359
344
  return createGatewayImageModel(modelId, config.provider)
360
345
  }
361
346
 
362
- console.error(pc.red(`No API key available for provider: ${config.provider}`))
363
- process.exit(1)
347
+ return new ValidationError(`No API key available for provider: ${config.provider}`)
364
348
  }
365
349
 
366
- export async function createTextModel(modelId: string): Promise<LanguageModel> {
350
+ export async function createTextModel(modelId: string): Promise<Error | LanguageModel> {
367
351
  const config = getModelConfig(modelId)
352
+ if (config instanceof Error) return config
353
+
368
354
  if (config.strategy !== 'text') {
369
- console.error(pc.red(`Model ${modelId} is not a text model`))
370
- process.exit(1)
355
+ return new ValidationError(`Model ${modelId} is not a text model`)
371
356
  }
372
- ensureProviderKey(config.provider)
357
+
358
+ const keyError = ensureProviderKey(config.provider)
359
+ if (keyError) return keyError
373
360
 
374
361
  const authSource = resolveAuthSource(config.provider)
375
362
  logAuthSource(authSource)
@@ -379,30 +366,26 @@ export async function createTextModel(modelId: string): Promise<LanguageModel> {
379
366
  if (factory) {
380
367
  return factory(modelId)
381
368
  }
382
- console.error(
383
- pc.red(
384
- `Text+image generation is not supported for provider: ${config.provider}`,
385
- ),
386
- )
387
- process.exit(1)
369
+ return new ValidationError(`Text+image generation is not supported for provider: ${config.provider}`)
388
370
  }
389
371
 
390
372
  if (hasEgakiKey()) {
391
373
  return createGatewayTextModel(modelId, config.provider)
392
374
  }
393
375
 
394
- console.error(pc.red(`No API key available for provider: ${config.provider}`))
395
- process.exit(1)
376
+ return new ValidationError(`No API key available for provider: ${config.provider}`)
396
377
  }
397
378
 
398
- export async function createVideoModel(modelId: string): Promise<any> {
379
+ export async function createVideoModel(modelId: string): Promise<Error | any> {
399
380
  const config = getModelConfig(modelId)
381
+ if (config instanceof Error) return config
382
+
400
383
  if (config.strategy !== 'video') {
401
- console.error(pc.red(`Model ${modelId} is not a video model`))
402
- process.exit(1)
384
+ return new ValidationError(`Model ${modelId} is not a video model`)
403
385
  }
404
386
 
405
- ensureProviderKey(config.provider)
387
+ const keyError = ensureProviderKey(config.provider)
388
+ if (keyError) return keyError
406
389
 
407
390
  const authSource = resolveAuthSource(config.provider)
408
391
  logAuthSource(authSource)
@@ -410,20 +393,16 @@ export async function createVideoModel(modelId: string): Promise<any> {
410
393
  if (hasDirectProviderKey(config.provider)) {
411
394
  const factory = PROVIDER_SDKS[config.provider]?.video
412
395
  if (factory) {
413
- return factory(modelId)
396
+ const result = await factory(modelId)
397
+ if (result instanceof Error) return result
398
+ return result
414
399
  }
415
- console.error(
416
- pc.red(
417
- `Direct video generation is not supported for provider: ${config.provider}. Use an egaki subscription instead.`,
418
- ),
419
- )
420
- process.exit(1)
400
+ return new ValidationError(`Direct video generation is not supported for provider: ${config.provider}. Use an egaki subscription instead.`)
421
401
  }
422
402
 
423
403
  if (hasEgakiKey()) {
424
404
  return createGatewayVideoModel(modelId, config.provider)
425
405
  }
426
406
 
427
- console.error(pc.red(`No API key available for provider: ${config.provider}`))
428
- process.exit(1)
407
+ return new ValidationError(`No API key available for provider: ${config.provider}`)
429
408
  }
@@ -0,0 +1,34 @@
1
+ // OpenAI speech provider for egaki.
2
+ // Wraps the Vercel AI SDK @ai-sdk/openai since OpenAI TTS has no
3
+ // timestamp support. Returns audio only, timestamps are always undefined.
4
+ import {
5
+ experimental_generateSpeech as aiGenerateSpeech,
6
+ } from 'ai'
7
+ import type { SpeechProvider, SpeechProviderOptions, SpeechProviderResult } from './speech-generate.js'
8
+
9
+ export function createOpenAISpeechProvider(): SpeechProvider {
10
+ return {
11
+ async generate(options: SpeechProviderOptions): Promise<SpeechProviderResult> {
12
+ const { openai } = await import('@ai-sdk/openai')
13
+ const model = openai.speech(options.modelId)
14
+
15
+ const result = await aiGenerateSpeech({
16
+ model,
17
+ text: options.text,
18
+ ...(options.voice ? { voice: options.voice } : {}),
19
+ ...(options.outputFormat ? { outputFormat: options.outputFormat } : {}),
20
+ ...(options.instructions ? { instructions: options.instructions } : {}),
21
+ ...(options.speed != null ? { speed: options.speed } : {}),
22
+ ...(options.language ? { language: options.language } : {}),
23
+ ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
24
+ })
25
+
26
+ return {
27
+ audio: result.audio.uint8Array,
28
+ mediaType: result.audio.mediaType || 'audio/mpeg',
29
+ // OpenAI TTS does not support word timestamps
30
+ timestamps: undefined,
31
+ }
32
+ },
33
+ }
34
+ }
@@ -0,0 +1,187 @@
1
+ // Audio separation using fal.ai's Demucs API.
2
+ // Splits audio into stems (vocals, drums, bass, guitar, piano, other).
3
+ // Returns Error | Result (errore style) instead of throwing.
4
+ //
5
+ // Uses @fal-ai/client directly since Demucs is a fal-specific API
6
+ // with no Vercel AI SDK wrapper. Requires FAL_KEY env var or
7
+ // egaki login --provider fal.
8
+ import { fal } from '@fal-ai/client'
9
+ import { injectCredentialsToEnv } from './credentials.js'
10
+
11
+ // ─── types ──────────────────────────────────────────────────────────────────
12
+
13
+ export type StemName = 'vocals' | 'drums' | 'bass' | 'other' | 'guitar' | 'piano'
14
+
15
+ export type DemucsModel =
16
+ | 'htdemucs'
17
+ | 'htdemucs_ft'
18
+ | 'htdemucs_6s'
19
+ | 'hdemucs_mmi'
20
+ | 'mdx'
21
+ | 'mdx_extra'
22
+ | 'mdx_q'
23
+ | 'mdx_extra_q'
24
+
25
+ export interface SeparateAudioOptions {
26
+ /** Raw audio bytes to separate. */
27
+ audio: Uint8Array
28
+ /** Which stems to extract. Default: ['vocals', 'other'] */
29
+ stems?: StemName[]
30
+ /** Demucs model variant. Default: 'htdemucs_6s' */
31
+ model?: DemucsModel
32
+ /** Output format: 'mp3' or 'wav'. Default: 'mp3' */
33
+ outputFormat?: 'mp3' | 'wav'
34
+ /** Number of random shifts for quality (higher = better but slower). Default: 1 */
35
+ shifts?: number
36
+ /** Segment overlap 0.0-1.0 (higher = better quality). Default: 0.25 */
37
+ overlap?: number
38
+ /** Original filename for display/logging. */
39
+ filename?: string
40
+ /** Progress callback for queue updates. */
41
+ onProgress?: (message: string) => void
42
+ }
43
+
44
+ export interface StemResult {
45
+ name: StemName
46
+ url: string
47
+ contentType: string
48
+ }
49
+
50
+ export interface SeparateAudioResult {
51
+ stems: StemResult[]
52
+ model: string
53
+ }
54
+
55
+ // ─── validation ─────────────────────────────────────────────────────────────
56
+
57
+ const ALL_STEMS: StemName[] = ['vocals', 'drums', 'bass', 'other', 'guitar', 'piano']
58
+ const FOUR_STEM_MODELS: DemucsModel[] = ['htdemucs', 'htdemucs_ft', 'hdemucs_mmi', 'mdx', 'mdx_extra', 'mdx_q', 'mdx_extra_q']
59
+ const SIX_STEM_ONLY: StemName[] = ['guitar', 'piano']
60
+
61
+ export function validateStems(stems: StemName[], model: DemucsModel): Error | void {
62
+ for (const s of stems) {
63
+ if (!ALL_STEMS.includes(s)) {
64
+ return new Error(`Unknown stem: "${s}". Valid stems: ${ALL_STEMS.join(', ')}`)
65
+ }
66
+ }
67
+ if (FOUR_STEM_MODELS.includes(model)) {
68
+ const invalid = stems.filter((s) => SIX_STEM_ONLY.includes(s))
69
+ if (invalid.length > 0) {
70
+ return new Error(
71
+ `Stems ${invalid.join(', ')} are only available with htdemucs_6s model. ` +
72
+ `Current model: ${model}`,
73
+ )
74
+ }
75
+ }
76
+ }
77
+
78
+ // ─── main function ──────────────────────────────────────────────────────────
79
+
80
+ export async function separateAudioUncached(
81
+ opts: SeparateAudioOptions,
82
+ ): Promise<Error | SeparateAudioResult> {
83
+ injectCredentialsToEnv()
84
+
85
+ const falKey = process.env['FAL_KEY']
86
+ if (!falKey) {
87
+ return new Error(
88
+ 'FAL_KEY not found. Run: egaki login --provider fal\n' +
89
+ 'Get your key at https://fal.ai/dashboard/keys',
90
+ )
91
+ }
92
+
93
+ fal.config({ credentials: falKey })
94
+
95
+ const model: DemucsModel = opts.model ?? 'htdemucs_6s'
96
+ const stems: StemName[] = opts.stems ?? ['vocals', 'other']
97
+ const outputFormat = opts.outputFormat ?? 'mp3'
98
+
99
+ const validationError = validateStems(stems, model)
100
+ if (validationError) return validationError
101
+
102
+ if (opts.audio.length === 0) {
103
+ return new Error('No audio data provided')
104
+ }
105
+
106
+ // Upload the audio file to fal storage so the API can access it
107
+ const file = new File(
108
+ [Buffer.from(opts.audio)],
109
+ opts.filename ?? 'audio.mp3',
110
+ { type: 'audio/mpeg' },
111
+ )
112
+
113
+ opts.onProgress?.('Uploading audio to fal.ai...')
114
+ let audioUrl: string
115
+ try {
116
+ audioUrl = await fal.storage.upload(file)
117
+ } catch (err) {
118
+ return new Error(`Failed to upload audio: ${err instanceof Error ? err.message : String(err)}`)
119
+ }
120
+ opts.onProgress?.('Audio uploaded. Starting separation...')
121
+
122
+ // Call the Demucs API
123
+ type DemucsOutput = Record<string, { url: string; content_type: string }>
124
+ let result: { data: DemucsOutput }
125
+ try {
126
+ result = await fal.subscribe('fal-ai/demucs', {
127
+ input: {
128
+ audio_url: audioUrl,
129
+ model,
130
+ stems,
131
+ shifts: opts.shifts ?? 1,
132
+ overlap: opts.overlap ?? 0.25,
133
+ output_format: outputFormat,
134
+ },
135
+ logs: true,
136
+ onQueueUpdate: (update) => {
137
+ if (update.status === 'IN_PROGRESS') {
138
+ const u = update as typeof update & { logs?: Array<{ message: string }> }
139
+ if (u.logs) {
140
+ for (const log of u.logs) {
141
+ opts.onProgress?.(log.message)
142
+ }
143
+ }
144
+ }
145
+ },
146
+ }) as any
147
+ } catch (err) {
148
+ return new Error(`Separation failed: ${err instanceof Error ? err.message : String(err)}`)
149
+ }
150
+
151
+ // Extract the requested stems from the result
152
+ const resultStems: StemResult[] = []
153
+ for (const stemName of stems) {
154
+ const stemData = result.data[stemName]
155
+ if (stemData?.url) {
156
+ resultStems.push({
157
+ name: stemName,
158
+ url: stemData.url,
159
+ contentType: stemData.content_type ?? `audio/${outputFormat}`,
160
+ })
161
+ }
162
+ }
163
+
164
+ if (resultStems.length === 0) {
165
+ return new Error('No stems returned from the API. Check model and stem names.')
166
+ }
167
+
168
+ return {
169
+ stems: resultStems,
170
+ model,
171
+ }
172
+ }
173
+
174
+ // ─── download helper ────────────────────────────────────────────────────────
175
+
176
+ /** Download a stem from its URL and return the bytes. */
177
+ export async function downloadStem(url: string): Promise<Error | Uint8Array> {
178
+ try {
179
+ const response = await fetch(url)
180
+ if (!response.ok) {
181
+ return new Error(`Failed to download stem: HTTP ${response.status}`)
182
+ }
183
+ return new Uint8Array(await response.arrayBuffer())
184
+ } catch (err) {
185
+ return new Error(`Failed to download stem: ${err instanceof Error ? err.message : String(err)}`)
186
+ }
187
+ }