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,408 @@
|
|
|
1
|
+
// Model registry for egaki.
|
|
2
|
+
// Maps model IDs to their provider and generation strategy. The enum of
|
|
3
|
+
// supported models is derived from this registry so there's a single source
|
|
4
|
+
// of truth for both CLI validation and runtime provider resolution.
|
|
5
|
+
//
|
|
6
|
+
// Provider SDK factories are stored in a descriptor map (PROVIDER_SDKS) so
|
|
7
|
+
// adding a new provider is one map entry instead of touching 3 switch blocks.
|
|
8
|
+
//
|
|
9
|
+
// Provider resolution priority:
|
|
10
|
+
// 1. Provider-specific auth (API key, or xAI/ChatGPT OAuth) → direct/custom backend
|
|
11
|
+
// 2. Egaki API key (EGAKI_API_KEY) → route through egaki gateway
|
|
12
|
+
// 3. No key → error with subscription recommendation
|
|
13
|
+
import type { ImageModel, LanguageModel } from 'ai'
|
|
14
|
+
import pc from 'picocolors'
|
|
15
|
+
import {
|
|
16
|
+
PROVIDERS,
|
|
17
|
+
EGAKI_GATEWAY_URL,
|
|
18
|
+
shouldUseChatGptBackend,
|
|
19
|
+
shouldUseXaiOAuth,
|
|
20
|
+
getXaiAuth,
|
|
21
|
+
saveXaiAuth,
|
|
22
|
+
} from './credentials.js'
|
|
23
|
+
import { getValidXaiAuth } from './xai-auth.js'
|
|
24
|
+
import {
|
|
25
|
+
CATALOG,
|
|
26
|
+
VIDEO_CATALOG,
|
|
27
|
+
findModel,
|
|
28
|
+
findVideoModel,
|
|
29
|
+
findAnyModel,
|
|
30
|
+
type ImageModelEntry,
|
|
31
|
+
type VideoModelEntry,
|
|
32
|
+
type AnyModelEntry,
|
|
33
|
+
} from './model-catalog.js'
|
|
34
|
+
|
|
35
|
+
export type { ImageModelEntry, VideoModelEntry, AnyModelEntry }
|
|
36
|
+
/** @deprecated Use ImageModelEntry instead */
|
|
37
|
+
export type ModelEntry = ImageModelEntry
|
|
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
|
+
|
|
50
|
+
export const IMAGE_MODELS = CATALOG.map((m) => m.id) as [string, ...string[]]
|
|
51
|
+
export const VIDEO_MODELS = VIDEO_CATALOG.map((m) => m.id) as [string, ...string[]]
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Strip provider prefix from a model ID (e.g. "vertex/imagen-4.0-generate-001" → "imagen-4.0-generate-001").
|
|
55
|
+
* Provider SDKs expect bare model IDs, but the catalog uses prefixed IDs for routing.
|
|
56
|
+
*/
|
|
57
|
+
function stripProviderPrefix(modelId: string): string {
|
|
58
|
+
const slash = modelId.indexOf('/')
|
|
59
|
+
if (slash === -1) return modelId
|
|
60
|
+
return modelId.slice(slash + 1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_MODEL = 'nano-banana-pro-preview'
|
|
64
|
+
export const DEFAULT_VIDEO_MODEL = 'veo-3.1-fast-generate-001'
|
|
65
|
+
|
|
66
|
+
export function getModelConfig(modelId: string): Error | AnyModelEntry {
|
|
67
|
+
const entry = findAnyModel(modelId)
|
|
68
|
+
if (!entry) {
|
|
69
|
+
return new ValidationError(`Unknown model: ${modelId}`)
|
|
70
|
+
}
|
|
71
|
+
return entry
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── provider SDK descriptor map ─────────────────────────────────────────────
|
|
75
|
+
// Each provider declares which model factories it supports. Lazy imports keep
|
|
76
|
+
// unused provider SDKs out of the startup path.
|
|
77
|
+
//
|
|
78
|
+
// To add a new provider:
|
|
79
|
+
// 1. Add an entry here with the relevant factory functions.
|
|
80
|
+
// 2. Add models to CATALOG/VIDEO_CATALOG in model-catalog.ts.
|
|
81
|
+
// 3. Add a PROVIDERS entry in credentials.ts for the env var / login hint.
|
|
82
|
+
// That's it — no switch statements to touch.
|
|
83
|
+
|
|
84
|
+
type ProviderSdk = {
|
|
85
|
+
image?: (modelId: string) => Promise<Error | ImageModel>
|
|
86
|
+
text?: (modelId: string) => Promise<Error | LanguageModel>
|
|
87
|
+
video?: (modelId: string) => Promise<Error | any>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Vertex is the only provider that uses a prefix (vertex/model-id) in the catalog
|
|
91
|
+
// but expects a bare model ID in its SDK. Replicate and fal use slashes as part
|
|
92
|
+
// of the actual model name (e.g. fal-ai/flux/schnell, black-forest-labs/flux-1.1-pro).
|
|
93
|
+
// stripProviderPrefix is called inside the Vertex lambdas only.
|
|
94
|
+
const PROVIDER_SDKS: Record<string, ProviderSdk> = {
|
|
95
|
+
google: {
|
|
96
|
+
image: async (id) => (await import('@ai-sdk/google')).google.image(id),
|
|
97
|
+
text: async (id) => (await import('@ai-sdk/google')).google(id),
|
|
98
|
+
video: async (id) => (await import('@ai-sdk/google')).google.video(id),
|
|
99
|
+
},
|
|
100
|
+
vertex: {
|
|
101
|
+
image: async (id) => (await import('@ai-sdk/google-vertex')).vertex.image(stripProviderPrefix(id)),
|
|
102
|
+
text: async (id) => (await import('@ai-sdk/google-vertex')).vertex(stripProviderPrefix(id)),
|
|
103
|
+
video: async (id) => (await import('@ai-sdk/google-vertex')).vertex.video(stripProviderPrefix(id)),
|
|
104
|
+
},
|
|
105
|
+
openai: {
|
|
106
|
+
image: async (id) => (await import('@ai-sdk/openai')).openai.image(id),
|
|
107
|
+
},
|
|
108
|
+
replicate: {
|
|
109
|
+
image: async (id) => (await import('@ai-sdk/replicate')).replicate.image(id),
|
|
110
|
+
},
|
|
111
|
+
fal: {
|
|
112
|
+
image: async (id) => (await import('@ai-sdk/fal')).fal.image(id),
|
|
113
|
+
video: async (id) => (await import('@ai-sdk/fal')).fal.video(id),
|
|
114
|
+
},
|
|
115
|
+
xai: {
|
|
116
|
+
image: async (id) => {
|
|
117
|
+
const xaiProvider = await getXaiProviderInstance()
|
|
118
|
+
if (xaiProvider instanceof Error) return xaiProvider
|
|
119
|
+
return xaiProvider.image(id)
|
|
120
|
+
},
|
|
121
|
+
video: async (id) => {
|
|
122
|
+
const xaiProvider = await getXaiProviderInstance()
|
|
123
|
+
if (xaiProvider instanceof Error) return xaiProvider
|
|
124
|
+
return xaiProvider.video(id)
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
// Providers routed exclusively through the egaki gateway (no direct SDK).
|
|
128
|
+
// They only need catalog entries and gateway routing, no local SDK factory.
|
|
129
|
+
// bfl, recraft, klingai, alibaba — all handled by the gateway fallback.
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── xAI provider instance ───────────────────────────────────────────────────
|
|
133
|
+
// Creates an @ai-sdk/xai provider instance with either a direct API key or
|
|
134
|
+
// OAuth tokens from the Grok Build subscription. OAuth tokens are injected via
|
|
135
|
+
// a custom fetch that replaces the Authorization header on every request.
|
|
136
|
+
|
|
137
|
+
async function getXaiProviderInstance(): Promise<Error | ReturnType<Awaited<typeof import('@ai-sdk/xai')>['createXai']>> {
|
|
138
|
+
const { createXai } = await import('@ai-sdk/xai')
|
|
139
|
+
|
|
140
|
+
// Direct API key path (env or stored)
|
|
141
|
+
if (!shouldUseXaiOAuth()) {
|
|
142
|
+
return createXai({
|
|
143
|
+
apiKey: process.env['XAI_API_KEY'],
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// OAuth path: inject bearer token via custom fetch
|
|
148
|
+
const storedAuth = getXaiAuth()
|
|
149
|
+
if (!storedAuth) {
|
|
150
|
+
return new ValidationError('Missing xAI OAuth tokens. Please run `egaki login` and select xAI Grok Build.')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const auth = await getValidXaiAuth(storedAuth, saveXaiAuth)
|
|
154
|
+
if (auth instanceof Error) {
|
|
155
|
+
return auth
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return createXai({
|
|
159
|
+
// Dummy key to satisfy the SDK's apiKey requirement; the custom fetch
|
|
160
|
+
// overrides the Authorization header with the real OAuth token.
|
|
161
|
+
apiKey: 'xai-oauth-placeholder',
|
|
162
|
+
fetch: async (input, init) => {
|
|
163
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined)
|
|
164
|
+
if (init?.headers) {
|
|
165
|
+
const initHeaders = new Headers(init.headers)
|
|
166
|
+
initHeaders.forEach((value, key) => headers.set(key, value))
|
|
167
|
+
}
|
|
168
|
+
headers.set('authorization', `Bearer ${auth.access}`)
|
|
169
|
+
return fetch(input, { ...init, headers })
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── key checking ────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function hasEgakiKey(): boolean {
|
|
177
|
+
const info = PROVIDERS['egaki']
|
|
178
|
+
if (!info) return false
|
|
179
|
+
return Boolean(process.env[info.envVar])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hasDirectProviderKey(providerName: string): boolean {
|
|
183
|
+
const info = PROVIDERS[providerName]
|
|
184
|
+
if (!info) return false
|
|
185
|
+
if (Boolean(process.env[info.envVar])) return true
|
|
186
|
+
// xAI OAuth counts as having a direct provider key since we use @ai-sdk/xai directly
|
|
187
|
+
if (providerName === 'xai' && shouldUseXaiOAuth()) return true
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check that the provider's API key or OAuth login is available before making API calls.
|
|
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
|
|
195
|
+
|
|
196
|
+
// Vertex models require a direct key — the upstream Vercel AI Gateway
|
|
197
|
+
// does not support vertex/ routing, so egaki gateway can't proxy them.
|
|
198
|
+
if (providerName === 'vertex') {
|
|
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
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (hasEgakiKey()) return undefined
|
|
207
|
+
|
|
208
|
+
const info = PROVIDERS[providerName]
|
|
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}` : ''),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── gateway model factories ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
async function createGatewayImageModel(modelId: string, provider: string): Promise<ImageModel> {
|
|
220
|
+
const { createGateway } = await import('ai')
|
|
221
|
+
const gateway = createGateway({
|
|
222
|
+
apiKey: process.env['EGAKI_API_KEY']!,
|
|
223
|
+
baseURL: EGAKI_GATEWAY_URL,
|
|
224
|
+
})
|
|
225
|
+
const bareId = stripProviderPrefix(modelId)
|
|
226
|
+
return gateway.image(`${provider}/${bareId}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function createGatewayTextModel(modelId: string, provider: string): Promise<LanguageModel> {
|
|
230
|
+
const { createGateway } = await import('ai')
|
|
231
|
+
const gateway = createGateway({
|
|
232
|
+
apiKey: process.env['EGAKI_API_KEY']!,
|
|
233
|
+
baseURL: EGAKI_GATEWAY_URL,
|
|
234
|
+
})
|
|
235
|
+
const bareId = stripProviderPrefix(modelId)
|
|
236
|
+
return gateway(`${provider}/${bareId}`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function createGatewayVideoModel(modelId: string, provider: string) {
|
|
240
|
+
const { createGateway } = await import('ai')
|
|
241
|
+
const gateway = createGateway({
|
|
242
|
+
apiKey: process.env['EGAKI_API_KEY']!,
|
|
243
|
+
baseURL: EGAKI_GATEWAY_URL,
|
|
244
|
+
})
|
|
245
|
+
const bareId = stripProviderPrefix(modelId)
|
|
246
|
+
return gateway.video(`${provider}/${bareId}`)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── ChatGPT OAuth routing ───────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* ChatGPT OAuth image generation goes through the Codex backend instead of the
|
|
253
|
+
* OpenAI Image API, so OpenAI image models need the custom responses path.
|
|
254
|
+
*/
|
|
255
|
+
export function shouldUseResponsesApi(modelId: string): boolean {
|
|
256
|
+
const config = findModel(modelId)
|
|
257
|
+
if (!config) return false
|
|
258
|
+
return config.provider === 'openai' && config.strategy === 'image' && shouldUseChatGptBackend()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── auth source detection ───────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
type AuthSource =
|
|
264
|
+
| { type: 'api-key'; label: string }
|
|
265
|
+
| { type: 'oauth'; label: string }
|
|
266
|
+
| { type: 'egaki-gateway' }
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Determine which auth source will be used for a given provider.
|
|
270
|
+
* Returns the auth type for logging. Does NOT check if the key is valid.
|
|
271
|
+
*/
|
|
272
|
+
function resolveAuthSource(providerName: string): AuthSource {
|
|
273
|
+
const info = PROVIDERS[providerName]
|
|
274
|
+
|
|
275
|
+
// xAI has special OAuth handling
|
|
276
|
+
if (providerName === 'xai') {
|
|
277
|
+
if (process.env['XAI_API_KEY'] && !shouldUseXaiOAuth()) {
|
|
278
|
+
return { type: 'api-key', label: 'XAI_API_KEY' }
|
|
279
|
+
}
|
|
280
|
+
if (shouldUseXaiOAuth()) {
|
|
281
|
+
return { type: 'oauth', label: 'xAI Grok Build OAuth' }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ChatGPT OAuth
|
|
286
|
+
if (providerName === 'openai' && shouldUseChatGptBackend()) {
|
|
287
|
+
return { type: 'oauth', label: 'ChatGPT OAuth' }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Direct provider env var
|
|
291
|
+
if (info && process.env[info.envVar]) {
|
|
292
|
+
return { type: 'api-key', label: info.envVar }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Egaki gateway
|
|
296
|
+
if (hasEgakiKey()) {
|
|
297
|
+
return { type: 'egaki-gateway' }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Should not reach here if ensureProviderKey passed
|
|
301
|
+
return { type: 'api-key', label: 'unknown' }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function logAuthSource(source: AuthSource): void {
|
|
305
|
+
switch (source.type) {
|
|
306
|
+
case 'api-key':
|
|
307
|
+
console.error(pc.dim(`Auth: ${source.label}`))
|
|
308
|
+
break
|
|
309
|
+
case 'oauth':
|
|
310
|
+
console.error(pc.dim(`Auth: ${source.label}`))
|
|
311
|
+
break
|
|
312
|
+
case 'egaki-gateway':
|
|
313
|
+
console.error(pc.dim('Auth: Egaki subscription (gateway)'))
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── public model factories ──────────────────────────────────────────────────
|
|
319
|
+
// Priority for xAI: explicit XAI_API_KEY > xai-oauth tokens > egaki gateway > error.
|
|
320
|
+
// Priority for others: direct provider key > egaki gateway > error.
|
|
321
|
+
// Uses PROVIDER_SDKS map for direct provider creation, eliminating switch blocks.
|
|
322
|
+
|
|
323
|
+
export async function createImageModel(modelId: string): Promise<Error | ImageModel> {
|
|
324
|
+
const config = getModelConfig(modelId)
|
|
325
|
+
if (config instanceof Error) return config
|
|
326
|
+
|
|
327
|
+
const keyError = ensureProviderKey(config.provider)
|
|
328
|
+
if (keyError) return keyError
|
|
329
|
+
|
|
330
|
+
const authSource = resolveAuthSource(config.provider)
|
|
331
|
+
logAuthSource(authSource)
|
|
332
|
+
|
|
333
|
+
if (hasDirectProviderKey(config.provider)) {
|
|
334
|
+
const factory = PROVIDER_SDKS[config.provider]?.image
|
|
335
|
+
if (factory) {
|
|
336
|
+
const result = await factory(modelId)
|
|
337
|
+
if (result instanceof Error) return result
|
|
338
|
+
return result
|
|
339
|
+
}
|
|
340
|
+
return new ValidationError(`Direct image generation is not supported for provider: ${config.provider}.`)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (hasEgakiKey()) {
|
|
344
|
+
return createGatewayImageModel(modelId, config.provider)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return new ValidationError(`No API key available for provider: ${config.provider}`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function createTextModel(modelId: string): Promise<Error | LanguageModel> {
|
|
351
|
+
const config = getModelConfig(modelId)
|
|
352
|
+
if (config instanceof Error) return config
|
|
353
|
+
|
|
354
|
+
if (config.strategy !== 'text') {
|
|
355
|
+
return new ValidationError(`Model ${modelId} is not a text model`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const keyError = ensureProviderKey(config.provider)
|
|
359
|
+
if (keyError) return keyError
|
|
360
|
+
|
|
361
|
+
const authSource = resolveAuthSource(config.provider)
|
|
362
|
+
logAuthSource(authSource)
|
|
363
|
+
|
|
364
|
+
if (hasDirectProviderKey(config.provider)) {
|
|
365
|
+
const factory = PROVIDER_SDKS[config.provider]?.text
|
|
366
|
+
if (factory) {
|
|
367
|
+
return factory(modelId)
|
|
368
|
+
}
|
|
369
|
+
return new ValidationError(`Text+image generation is not supported for provider: ${config.provider}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (hasEgakiKey()) {
|
|
373
|
+
return createGatewayTextModel(modelId, config.provider)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return new ValidationError(`No API key available for provider: ${config.provider}`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function createVideoModel(modelId: string): Promise<Error | any> {
|
|
380
|
+
const config = getModelConfig(modelId)
|
|
381
|
+
if (config instanceof Error) return config
|
|
382
|
+
|
|
383
|
+
if (config.strategy !== 'video') {
|
|
384
|
+
return new ValidationError(`Model ${modelId} is not a video model`)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const keyError = ensureProviderKey(config.provider)
|
|
388
|
+
if (keyError) return keyError
|
|
389
|
+
|
|
390
|
+
const authSource = resolveAuthSource(config.provider)
|
|
391
|
+
logAuthSource(authSource)
|
|
392
|
+
|
|
393
|
+
if (hasDirectProviderKey(config.provider)) {
|
|
394
|
+
const factory = PROVIDER_SDKS[config.provider]?.video
|
|
395
|
+
if (factory) {
|
|
396
|
+
const result = await factory(modelId)
|
|
397
|
+
if (result instanceof Error) return result
|
|
398
|
+
return result
|
|
399
|
+
}
|
|
400
|
+
return new ValidationError(`Direct video generation is not supported for provider: ${config.provider}. Use an egaki subscription instead.`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (hasEgakiKey()) {
|
|
404
|
+
return createGatewayVideoModel(modelId, config.provider)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return new ValidationError(`No API key available for provider: ${config.provider}`)
|
|
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
|
+
}
|