egaki 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -9
- package/dist/cli/cache-utils.d.ts +33 -0
- package/dist/cli/cache-utils.d.ts.map +1 -0
- package/dist/cli/cache-utils.js +157 -0
- package/dist/cli/cached-generate.d.ts +105 -0
- package/dist/cli/cached-generate.d.ts.map +1 -0
- package/dist/cli/cached-generate.js +208 -0
- package/dist/cli/cached-generate.test.d.ts +11 -0
- package/dist/cli/cached-generate.test.d.ts.map +1 -0
- package/dist/cli/cached-generate.test.js +431 -0
- package/dist/cli/cartesia-provider.d.ts +16 -0
- package/dist/cli/cartesia-provider.d.ts.map +1 -0
- package/dist/cli/cartesia-provider.js +184 -0
- package/dist/cli/chatgpt-auth.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/{cli.js → cli/cli.js} +626 -600
- package/dist/cli/clone-generate.d.ts +26 -0
- package/dist/cli/clone-generate.d.ts.map +1 -0
- package/dist/cli/clone-generate.js +148 -0
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/{credentials.js → cli/credentials.js} +20 -0
- package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
- package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
- package/dist/cli/elevenlabs-speech-provider.js +199 -0
- package/dist/cli/generate.d.ts +140 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +604 -0
- package/dist/cli/generate.test.d.ts +2 -0
- package/dist/cli/generate.test.d.ts.map +1 -0
- package/dist/cli/generate.test.js +131 -0
- package/dist/cli/login.d.ts.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +7 -0
- package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
- package/dist/cli/model-catalog.d.ts.map +1 -0
- package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
- package/dist/{models.d.ts → cli/models.d.ts} +12 -5
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/{models.js → cli/models.js} +58 -57
- package/dist/cli/open-browser.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.d.ts +3 -0
- package/dist/cli/openai-speech-provider.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.js +28 -0
- package/dist/cli/separation-generate.d.ts +34 -0
- package/dist/cli/separation-generate.d.ts.map +1 -0
- package/dist/cli/separation-generate.js +117 -0
- package/dist/cli/speech-catalog.d.ts +35 -0
- package/dist/cli/speech-catalog.d.ts.map +1 -0
- package/dist/cli/speech-catalog.js +195 -0
- package/dist/cli/speech-generate.d.ts +86 -0
- package/dist/cli/speech-generate.d.ts.map +1 -0
- package/dist/cli/speech-generate.js +156 -0
- package/dist/cli/speech-models.d.ts +8 -0
- package/dist/cli/speech-models.d.ts.map +1 -0
- package/dist/cli/speech-models.js +109 -0
- package/dist/cli/speech-provider.test.d.ts +2 -0
- package/dist/cli/speech-provider.test.d.ts.map +1 -0
- package/dist/cli/speech-provider.test.js +197 -0
- package/dist/cli/subscription.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.d.ts +35 -0
- package/dist/cli/transcription-catalog.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.js +192 -0
- package/dist/cli/transcription-generate.d.ts +62 -0
- package/dist/cli/transcription-generate.d.ts.map +1 -0
- package/dist/cli/transcription-generate.js +144 -0
- package/dist/cli/transcription-models.d.ts +9 -0
- package/dist/cli/transcription-models.d.ts.map +1 -0
- package/dist/cli/transcription-models.js +100 -0
- package/dist/cli/transcription-providers.d.ts +22 -0
- package/dist/cli/transcription-providers.d.ts.map +1 -0
- package/dist/cli/transcription-providers.js +290 -0
- package/dist/cli/video-model-catalog.d.ts.map +1 -0
- package/dist/cli/xai-auth.d.ts.map +1 -0
- package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
- package/dist/vite/angled-screen.d.ts +57 -0
- package/dist/vite/angled-screen.d.ts.map +1 -0
- package/dist/vite/angled-screen.js +115 -0
- package/dist/vite/app.d.ts +51 -0
- package/dist/vite/app.d.ts.map +1 -0
- package/dist/vite/app.js +150 -0
- package/dist/vite/bands-shader.d.ts +47 -0
- package/dist/vite/bands-shader.d.ts.map +1 -0
- package/dist/vite/bands-shader.js +267 -0
- package/dist/vite/code-block.d.ts +107 -0
- package/dist/vite/code-block.d.ts.map +1 -0
- package/dist/vite/code-block.js +641 -0
- package/dist/vite/components.d.ts +36 -0
- package/dist/vite/components.d.ts.map +1 -0
- package/dist/vite/components.js +113 -0
- package/dist/vite/dispersion-rings-shader.d.ts +44 -0
- package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
- package/dist/vite/dispersion-rings-shader.js +282 -0
- package/dist/vite/easing-curves.d.ts +129 -0
- package/dist/vite/easing-curves.d.ts.map +1 -0
- package/dist/vite/easing-curves.js +584 -0
- package/dist/vite/easing-curves.test.d.ts +2 -0
- package/dist/vite/easing-curves.test.d.ts.map +1 -0
- package/dist/vite/easing-curves.test.js +200 -0
- package/dist/vite/generated-media-client.d.ts +35 -0
- package/dist/vite/generated-media-client.d.ts.map +1 -0
- package/dist/vite/generated-media-client.js +76 -0
- package/dist/vite/keyframes.d.ts +129 -0
- package/dist/vite/keyframes.d.ts.map +1 -0
- package/dist/vite/keyframes.js +194 -0
- package/dist/vite/layout-editor.d.ts +45 -0
- package/dist/vite/layout-editor.d.ts.map +1 -0
- package/dist/vite/layout-editor.js +587 -0
- package/dist/vite/layout-transition.d.ts +136 -0
- package/dist/vite/layout-transition.d.ts.map +1 -0
- package/dist/vite/layout-transition.js +581 -0
- package/dist/vite/liquid-gradient-shader.d.ts +62 -0
- package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
- package/dist/vite/liquid-gradient-shader.js +413 -0
- package/dist/vite/mdx-client.d.ts +15 -0
- package/dist/vite/mdx-client.d.ts.map +1 -0
- package/dist/vite/mdx-client.js +265 -0
- package/dist/vite/mdx-parse.d.ts +102 -0
- package/dist/vite/mdx-parse.d.ts.map +1 -0
- package/dist/vite/mdx-parse.js +251 -0
- package/dist/vite/mdx-provided-components.d.ts +18 -0
- package/dist/vite/mdx-provided-components.d.ts.map +1 -0
- package/dist/vite/mdx-provided-components.js +14 -0
- package/dist/vite/mdx-video.d.ts +327 -0
- package/dist/vite/mdx-video.d.ts.map +1 -0
- package/dist/vite/mdx-video.js +596 -0
- package/dist/vite/mdx-video.test.d.ts +9 -0
- package/dist/vite/mdx-video.test.d.ts.map +1 -0
- package/dist/vite/mdx-video.test.js +1945 -0
- package/dist/vite/media-components.d.ts +45 -0
- package/dist/vite/media-components.d.ts.map +1 -0
- package/dist/vite/media-components.js +355 -0
- package/dist/vite/media-duration-store.d.ts +66 -0
- package/dist/vite/media-duration-store.d.ts.map +1 -0
- package/dist/vite/media-duration-store.js +204 -0
- package/dist/vite/player-page.d.ts +35 -0
- package/dist/vite/player-page.d.ts.map +1 -0
- package/dist/vite/player-page.js +720 -0
- package/dist/vite/render-client.d.ts +18 -0
- package/dist/vite/render-client.d.ts.map +1 -0
- package/dist/vite/render-client.js +67 -0
- package/dist/vite/sdk.d.ts +221 -0
- package/dist/vite/sdk.d.ts.map +1 -0
- package/dist/vite/sdk.js +355 -0
- package/dist/vite/server-components.d.ts +55 -0
- package/dist/vite/server-components.d.ts.map +1 -0
- package/dist/vite/server-components.js +156 -0
- package/dist/vite/server-mdx.d.ts +74 -0
- package/dist/vite/server-mdx.d.ts.map +1 -0
- package/dist/vite/server-mdx.js +259 -0
- package/dist/vite/shader-renderer.d.ts +48 -0
- package/dist/vite/shader-renderer.d.ts.map +1 -0
- package/dist/vite/shader-renderer.js +411 -0
- package/dist/vite/store-hooks.d.ts +12 -0
- package/dist/vite/store-hooks.d.ts.map +1 -0
- package/dist/vite/store-hooks.js +93 -0
- package/dist/vite/store.d.ts +75 -0
- package/dist/vite/store.d.ts.map +1 -0
- package/dist/vite/store.js +52 -0
- package/dist/vite/store.test.d.ts +6 -0
- package/dist/vite/store.test.d.ts.map +1 -0
- package/dist/vite/store.test.js +172 -0
- package/dist/vite/tweakpane-hook.d.ts +94 -0
- package/dist/vite/tweakpane-hook.d.ts.map +1 -0
- package/dist/vite/tweakpane-hook.js +450 -0
- package/dist/vite/vite-plugin.d.ts +28 -0
- package/dist/vite/vite-plugin.d.ts.map +1 -0
- package/dist/vite/vite-plugin.js +538 -0
- package/dist/vite/wave-gradient-shader.d.ts +41 -0
- package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
- package/dist/vite/wave-gradient-shader.js +207 -0
- package/package.json +101 -24
- package/skills/egaki/SKILL.md +162 -1
- package/src/cli/cache-utils.ts +169 -0
- package/src/cli/cached-generate.test.ts +531 -0
- package/src/cli/cached-generate.ts +348 -0
- package/src/cli/cartesia-provider.ts +249 -0
- package/src/{cli.ts → cli/cli.ts} +836 -884
- package/src/cli/clone-generate.ts +202 -0
- package/src/{credentials.ts → cli/credentials.ts} +20 -0
- package/src/cli/elevenlabs-speech-provider.ts +243 -0
- package/src/cli/generate.test.ts +166 -0
- package/src/cli/generate.ts +912 -0
- package/src/cli/main.ts +8 -0
- package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
- package/src/{models.ts → cli/models.ts} +65 -86
- package/src/cli/openai-speech-provider.ts +34 -0
- package/src/cli/separation-generate.ts +187 -0
- package/src/cli/speech-catalog.ts +253 -0
- package/src/cli/speech-generate.ts +275 -0
- package/src/cli/speech-models.ts +146 -0
- package/src/cli/speech-provider.test.ts +226 -0
- package/src/cli/transcription-catalog.ts +250 -0
- package/src/cli/transcription-generate.ts +214 -0
- package/src/cli/transcription-models.ts +136 -0
- package/src/cli/transcription-providers.ts +427 -0
- package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
- package/src/vite/angled-screen.tsx +214 -0
- package/src/vite/app.tsx +197 -0
- package/src/vite/bands-shader.tsx +319 -0
- package/src/vite/code-block.tsx +939 -0
- package/src/vite/components.tsx +278 -0
- package/src/vite/dispersion-rings-shader.tsx +331 -0
- package/src/vite/easing-curves.test.ts +232 -0
- package/src/vite/easing-curves.ts +743 -0
- package/src/vite/generated-media-client.tsx +121 -0
- package/src/vite/keyframes.tsx +347 -0
- package/src/vite/layout-editor.tsx +709 -0
- package/src/vite/layout-transition.tsx +706 -0
- package/src/vite/liquid-gradient-shader.tsx +474 -0
- package/src/vite/mdx-client.tsx +420 -0
- package/src/vite/mdx-parse.ts +343 -0
- package/src/vite/mdx-provided-components.ts +19 -0
- package/src/vite/mdx-video.test.tsx +2219 -0
- package/src/vite/mdx-video.tsx +976 -0
- package/src/vite/media-components.tsx +473 -0
- package/src/vite/media-duration-store.ts +225 -0
- package/src/vite/motion-timing.ts +194 -0
- package/src/vite/player-page.tsx +1107 -0
- package/src/vite/render-client.ts +84 -0
- package/src/vite/sdk.ts +598 -0
- package/src/vite/server-components.tsx +241 -0
- package/src/vite/server-mdx.ts +285 -0
- package/src/vite/shader-renderer.tsx +535 -0
- package/src/vite/store-hooks.ts +117 -0
- package/src/vite/store.test.ts +202 -0
- package/src/vite/store.ts +115 -0
- package/src/vite/styles.css +21 -0
- package/src/vite/tweakpane-hook.tsx +593 -0
- package/src/vite/virtual-modules.d.ts +30 -0
- package/src/vite/vite-plugin.ts +584 -0
- package/src/vite/wave-gradient-shader.tsx +253 -0
- package/README.md +0 -370
- package/dist/antigravity-auth.d.ts +0 -15
- package/dist/antigravity-auth.d.ts.map +0 -1
- package/dist/antigravity-auth.js +0 -544
- package/dist/antigravity-image.d.ts +0 -11
- package/dist/antigravity-image.d.ts.map +0 -1
- package/dist/antigravity-image.js +0 -285
- package/dist/chatgpt-auth.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.d.ts +0 -11
- package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.js +0 -64
- package/dist/login.d.ts.map +0 -1
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/models.d.ts.map +0 -1
- package/dist/open-browser.d.ts.map +0 -1
- package/dist/subscription.d.ts.map +0 -1
- package/dist/video-model-catalog.d.ts.map +0 -1
- package/dist/xai-auth.d.ts.map +0 -1
- /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
- /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
- /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
- /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
- /package/dist/{login.js → cli/login.js} +0 -0
- /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
- /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
- /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
- /package/dist/{subscription.js → cli/subscription.js} +0 -0
- /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
- /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
- /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
- /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
- /package/src/{login.ts → cli/login.ts} +0 -0
- /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
- /package/src/{subscription.ts → cli/subscription.ts} +0 -0
- /package/src/{video-model-catalog.ts → cli/video-model-catalog.ts} +0 -0
|
@@ -12,40 +12,24 @@ import { z } from 'zod'
|
|
|
12
12
|
import dedent from 'string-dedent'
|
|
13
13
|
import {
|
|
14
14
|
APICallError,
|
|
15
|
-
generateImage as aiGenerateImage,
|
|
16
|
-
generateText,
|
|
17
|
-
experimental_generateVideo as aiGenerateVideo,
|
|
18
15
|
NoImageGeneratedError,
|
|
19
16
|
NoVideoGeneratedError,
|
|
20
17
|
RetryError,
|
|
21
18
|
} from 'ai'
|
|
22
|
-
import type { XaiImageModelOptions, XaiVideoModelOptions } from '@ai-sdk/xai'
|
|
23
|
-
import type { GoogleImageModelOptions, GoogleLanguageModelOptions, GoogleVideoModelOptions } from '@ai-sdk/google'
|
|
24
|
-
import type { GoogleVertexImageModelOptions, GoogleVertexVideoModelOptions } from '@ai-sdk/google-vertex'
|
|
25
|
-
import type { FalImageModelOptions, FalVideoModelOptions } from '@ai-sdk/fal'
|
|
26
19
|
import { select, isCancel, cancel } from '@clack/prompts'
|
|
27
20
|
import fs from 'node:fs'
|
|
28
21
|
import path from 'node:path'
|
|
29
22
|
import pc from 'picocolors'
|
|
30
|
-
import
|
|
31
|
-
import pkg from '../package.json' with { type: 'json' }
|
|
23
|
+
import pkg from '../../package.json' with { type: 'json' }
|
|
32
24
|
import {
|
|
33
25
|
injectCredentialsToEnv,
|
|
34
26
|
PROVIDERS,
|
|
35
|
-
getChatGptAuth,
|
|
36
|
-
saveChatGptAuth,
|
|
37
27
|
getKeyStatus,
|
|
38
28
|
} from './credentials.js'
|
|
39
29
|
import {
|
|
40
|
-
IMAGE_MODELS,
|
|
41
|
-
VIDEO_MODELS,
|
|
42
30
|
DEFAULT_MODEL,
|
|
43
31
|
DEFAULT_VIDEO_MODEL,
|
|
44
32
|
getModelConfig,
|
|
45
|
-
createImageModel,
|
|
46
|
-
createTextModel,
|
|
47
|
-
createVideoModel,
|
|
48
|
-
shouldUseResponsesApi,
|
|
49
33
|
} from './models.js'
|
|
50
34
|
import {
|
|
51
35
|
CATALOG,
|
|
@@ -65,21 +49,44 @@ import {
|
|
|
65
49
|
unsubscribe,
|
|
66
50
|
showUsage,
|
|
67
51
|
} from './subscription.js'
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
52
|
+
import {
|
|
53
|
+
generateImageUncached as generateImage,
|
|
54
|
+
generateVideoUncached as generateVideo,
|
|
55
|
+
calculateCost,
|
|
56
|
+
ValidationError,
|
|
57
|
+
type GeneratedFile,
|
|
58
|
+
} from './generate.js'
|
|
59
|
+
import {
|
|
60
|
+
generateSpeechUncached as generateSpeech,
|
|
61
|
+
} from './speech-generate.js'
|
|
62
|
+
import {
|
|
63
|
+
transcribeAudioUncached as transcribeAudio,
|
|
64
|
+
segmentsToWordTimestamps,
|
|
65
|
+
} from './transcription-generate.js'
|
|
66
|
+
import {
|
|
67
|
+
SPEECH_CATALOG,
|
|
68
|
+
type SpeechModelEntry,
|
|
69
|
+
} from './speech-catalog.js'
|
|
70
|
+
import {
|
|
71
|
+
TRANSCRIPTION_CATALOG,
|
|
72
|
+
type TranscriptionModelEntry,
|
|
73
|
+
} from './transcription-catalog.js'
|
|
74
|
+
import {
|
|
75
|
+
DEFAULT_SPEECH_MODEL,
|
|
76
|
+
} from './speech-models.js'
|
|
77
|
+
import {
|
|
78
|
+
DEFAULT_TRANSCRIPTION_MODEL,
|
|
79
|
+
} from './transcription-models.js'
|
|
80
|
+
import {
|
|
81
|
+
separateAudioUncached as separateAudio,
|
|
82
|
+
downloadStem,
|
|
83
|
+
type StemName,
|
|
84
|
+
type DemucsModel,
|
|
85
|
+
} from './separation-generate.js'
|
|
86
|
+
import {
|
|
87
|
+
cloneVoiceUncached as cloneVoice,
|
|
88
|
+
type CloneProvider,
|
|
89
|
+
} from './clone-generate.js'
|
|
83
90
|
|
|
84
91
|
const cli = goke('egaki')
|
|
85
92
|
|
|
@@ -358,18 +365,13 @@ cli
|
|
|
358
365
|
.example('# Pipe to another tool')
|
|
359
366
|
.example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
|
|
360
367
|
.action(async (prompt, options) => {
|
|
361
|
-
// Inject stored API keys as env vars before calling the AI SDK.
|
|
362
|
-
injectCredentialsToEnv()
|
|
363
|
-
|
|
364
368
|
const model = options.model ?? await resolveImageModel()
|
|
365
369
|
const outputPath = options.output ?? 'egaki-output.png'
|
|
366
|
-
const count = options.count ?? 1
|
|
367
|
-
const config = getModelConfig(model)
|
|
368
|
-
const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio)
|
|
369
370
|
|
|
370
371
|
if (!options.stdout) {
|
|
371
372
|
console.error(pc.dim(`Model: ${model}`))
|
|
372
373
|
console.error(pc.dim(`Prompt: ${prompt}`))
|
|
374
|
+
console.error(pc.cyan('Generating...'))
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
const inputImages = await readInputImages(options.input)
|
|
@@ -377,56 +379,49 @@ cli
|
|
|
377
379
|
? await readInputSource(options.mask)
|
|
378
380
|
: undefined
|
|
379
381
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
const result = await runProviderCall('Image generation', () => generateImage({
|
|
383
|
+
prompt,
|
|
384
|
+
model,
|
|
385
|
+
count: options.count ?? 1,
|
|
386
|
+
aspectRatio: options.aspectRatio,
|
|
387
|
+
seed: options.seed,
|
|
388
|
+
inputImages,
|
|
389
|
+
maskImage,
|
|
390
|
+
allowPeople: options.allowPeople || false,
|
|
391
|
+
quality: options.quality,
|
|
392
|
+
resolution: options.resolution,
|
|
393
|
+
outputFormat: options.outputFormat,
|
|
394
|
+
negativePrompt: options.negativePrompt,
|
|
395
|
+
imageSize: options.imageSize,
|
|
396
|
+
}))
|
|
397
|
+
|
|
398
|
+
if (options.stdout) {
|
|
399
|
+
writeFirstToStdout(result.images)
|
|
400
|
+
return
|
|
401
|
+
}
|
|
384
402
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
json: options.json || false,
|
|
399
|
-
stdout: options.stdout || false,
|
|
400
|
-
})
|
|
401
|
-
} else if (config.strategy === 'image') {
|
|
402
|
-
await generateWithImageModel({
|
|
403
|
-
prompt,
|
|
404
|
-
model,
|
|
405
|
-
outputPath,
|
|
406
|
-
count,
|
|
407
|
-
aspectRatio: parsedAspectRatio,
|
|
408
|
-
seed: options.seed,
|
|
409
|
-
inputImages,
|
|
410
|
-
maskImage,
|
|
411
|
-
allowPeople: options.allowPeople || false,
|
|
412
|
-
quality: options.quality,
|
|
413
|
-
resolution: options.resolution,
|
|
414
|
-
outputFormat: options.outputFormat,
|
|
415
|
-
negativePrompt: options.negativePrompt,
|
|
416
|
-
json: options.json || false,
|
|
417
|
-
stdout: options.stdout || false,
|
|
418
|
-
})
|
|
419
|
-
} else {
|
|
420
|
-
await generateWithTextModel({
|
|
421
|
-
prompt,
|
|
403
|
+
const savedFiles = saveGeneratedFiles(result.images, outputPath)
|
|
404
|
+
printCost(result.cost)
|
|
405
|
+
|
|
406
|
+
if (result.revisedPrompt && !options.json) {
|
|
407
|
+
console.error(pc.dim(`Revised prompt: ${result.revisedPrompt}`))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (result.text && !options.json) {
|
|
411
|
+
console.error(pc.dim(result.text))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (options.json) {
|
|
415
|
+
console.log(JSON.stringify({
|
|
422
416
|
model,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
417
|
+
files: savedFiles,
|
|
418
|
+
count: result.images.length,
|
|
419
|
+
cost: result.cost,
|
|
420
|
+
usage: result.usage,
|
|
421
|
+
warnings: result.warnings,
|
|
422
|
+
...(result.text ? { text: result.text } : {}),
|
|
423
|
+
...(result.revisedPrompt !== undefined ? { revisedPrompt: result.revisedPrompt } : {}),
|
|
424
|
+
}, null, 2))
|
|
430
425
|
}
|
|
431
426
|
})
|
|
432
427
|
|
|
@@ -501,7 +496,7 @@ cli
|
|
|
501
496
|
z
|
|
502
497
|
.array(z.string())
|
|
503
498
|
.describe(
|
|
504
|
-
'Reference images for R2V generation, repeatable
|
|
499
|
+
'Reference images for R2V generation, repeatable. xAI: 1-7 images. Seedance: 1-9 images (use @Image1 etc. in prompt)',
|
|
505
500
|
),
|
|
506
501
|
)
|
|
507
502
|
.option(
|
|
@@ -535,18 +530,17 @@ cli
|
|
|
535
530
|
|
|
536
531
|
const model = options.model ?? await resolveVideoModel()
|
|
537
532
|
const outputPath = options.output ?? 'egaki-output.mp4'
|
|
538
|
-
const count = options.count ?? 1
|
|
539
533
|
const config = getModelConfig(model)
|
|
540
534
|
|
|
541
|
-
if (config
|
|
542
|
-
console.error(pc.red(
|
|
535
|
+
if (config instanceof Error) {
|
|
536
|
+
console.error(pc.red(config.message))
|
|
543
537
|
process.exit(1)
|
|
544
538
|
}
|
|
545
539
|
|
|
546
540
|
if (!options.stdout) {
|
|
547
541
|
console.error(pc.dim(`Model: ${model}`))
|
|
548
542
|
console.error(pc.dim(`Prompt: ${prompt}`))
|
|
549
|
-
console.error(pc.
|
|
543
|
+
console.error(pc.cyan('Generating...'))
|
|
550
544
|
}
|
|
551
545
|
|
|
552
546
|
// Determine how --input is used based on --mode:
|
|
@@ -554,28 +548,21 @@ cli
|
|
|
554
548
|
// - everything else (i2v, t2v): input is a reference image, passed as bytes
|
|
555
549
|
const isVideoInputMode = options.mode === 'edit-video' || options.mode === 'extend-video'
|
|
556
550
|
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
if (options.mode === 'reference-to-video' && (!options.referenceImages || options.referenceImages.length === 0)) {
|
|
563
|
-
console.error(pc.red('--reference-images is required with --mode reference-to-video. Pass 1-7 image files or URLs.'))
|
|
564
|
-
process.exit(1)
|
|
551
|
+
// Auto-set --mode reference-to-video for providers that require it (xAI) when
|
|
552
|
+
// --reference-images is provided without an explicit --mode.
|
|
553
|
+
if (options.referenceImages?.length && !options.mode && config.provider === 'xai') {
|
|
554
|
+
options.mode = 'reference-to-video'
|
|
565
555
|
}
|
|
566
556
|
|
|
567
557
|
let inputImage: Uint8Array | undefined
|
|
568
558
|
let videoUrl: string | undefined
|
|
569
559
|
|
|
570
560
|
if (options.input && isVideoInputMode) {
|
|
571
|
-
// Upload local file or use URL directly for video editing/extension
|
|
572
561
|
videoUrl = await resolveToUrl(options.input)
|
|
573
562
|
} else if (options.input) {
|
|
574
|
-
// Read as bytes for image-to-video
|
|
575
563
|
inputImage = await readInputSource(options.input)
|
|
576
564
|
}
|
|
577
565
|
|
|
578
|
-
// Resolve --reference-images: upload local files to get URLs
|
|
579
566
|
let resolvedReferenceImages = options.referenceImages
|
|
580
567
|
if (resolvedReferenceImages) {
|
|
581
568
|
resolvedReferenceImages = await Promise.all(
|
|
@@ -583,24 +570,587 @@ cli
|
|
|
583
570
|
)
|
|
584
571
|
}
|
|
585
572
|
|
|
586
|
-
await
|
|
587
|
-
prompt,
|
|
588
|
-
model,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
573
|
+
const result = await runProviderCall('Video generation', () => generateVideo({
|
|
574
|
+
prompt,
|
|
575
|
+
model,
|
|
576
|
+
count: options.count ?? 1,
|
|
577
|
+
aspectRatio: options.aspectRatio,
|
|
578
|
+
resolution: options.resolution,
|
|
579
|
+
duration: options.duration,
|
|
580
|
+
fps: options.fps,
|
|
581
|
+
seed: options.seed,
|
|
582
|
+
inputImage,
|
|
583
|
+
mode: options.mode,
|
|
584
|
+
videoUrl,
|
|
585
|
+
referenceImages: resolvedReferenceImages,
|
|
586
|
+
negativePrompt: options.negativePrompt,
|
|
587
|
+
}))
|
|
588
|
+
|
|
589
|
+
if (options.stdout) {
|
|
590
|
+
writeFirstToStdout(result.videos)
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const savedFiles = saveGeneratedFiles(result.videos, outputPath)
|
|
595
|
+
printCost(result.cost)
|
|
596
|
+
|
|
597
|
+
if (options.json) {
|
|
598
|
+
console.log(JSON.stringify({
|
|
599
|
+
model,
|
|
600
|
+
files: savedFiles,
|
|
601
|
+
count: result.videos.length,
|
|
602
|
+
cost: result.cost,
|
|
603
|
+
warnings: result.warnings,
|
|
604
|
+
responses: result.responses,
|
|
605
|
+
}, null, 2))
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
// ─── speech command ──────────────────────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
cli
|
|
612
|
+
.command(
|
|
613
|
+
'speech [text]',
|
|
614
|
+
dedent`
|
|
615
|
+
Generate speech audio from text using AI text-to-speech models.
|
|
616
|
+
Supports OpenAI (tts-1, tts-1-hd, gpt-4o-mini-tts), ElevenLabs
|
|
617
|
+
(eleven_v3, eleven_multilingual_v2, eleven_flash_v2_5), and Cartesia
|
|
618
|
+
(sonic-3.5, sonic-3). Uses your provider API key directly (egaki
|
|
619
|
+
subscription does not cover speech yet).
|
|
620
|
+
`,
|
|
621
|
+
)
|
|
622
|
+
.option(
|
|
623
|
+
'-m, --model [model]',
|
|
624
|
+
z.string().describe('Speech model ID. If omitted, shows an interactive picker (or uses default in non-TTY mode)'),
|
|
625
|
+
)
|
|
626
|
+
.option(
|
|
627
|
+
'-o, --output [path]',
|
|
628
|
+
z
|
|
629
|
+
.string()
|
|
630
|
+
.default('egaki-output')
|
|
631
|
+
.describe('Output file path (extension added from audio format if omitted)'),
|
|
632
|
+
)
|
|
633
|
+
.option(
|
|
634
|
+
'--voice [voice]',
|
|
635
|
+
z
|
|
636
|
+
.string()
|
|
637
|
+
.describe('Voice ID or name (provider-specific). OpenAI: alloy, echo, nova, etc. ElevenLabs/Cartesia: voice ID from their library'),
|
|
638
|
+
)
|
|
639
|
+
.option(
|
|
640
|
+
'--output-format [format]',
|
|
641
|
+
z
|
|
642
|
+
.string()
|
|
643
|
+
.describe('Audio output format: mp3, wav, pcm, opus, aac, flac (provider support varies)'),
|
|
644
|
+
)
|
|
645
|
+
.option(
|
|
646
|
+
'--speed [speed]',
|
|
647
|
+
z
|
|
648
|
+
.number()
|
|
649
|
+
.describe('Playback speed multiplier (OpenAI: 0.25-4.0, Cartesia: 0.6-1.5)'),
|
|
650
|
+
)
|
|
651
|
+
.option(
|
|
652
|
+
'--instructions [text]',
|
|
653
|
+
z
|
|
654
|
+
.string()
|
|
655
|
+
.describe('Style instructions for speech (only gpt-4o-mini-tts and ElevenLabs). E.g. "Speak in a calm, soothing tone"'),
|
|
656
|
+
)
|
|
657
|
+
.option(
|
|
658
|
+
'--language [lang]',
|
|
659
|
+
z
|
|
660
|
+
.string()
|
|
661
|
+
.describe('ISO 639-1 language code (e.g. en, es, fr). ElevenLabs and Cartesia'),
|
|
662
|
+
)
|
|
663
|
+
.option(
|
|
664
|
+
'--stdin',
|
|
665
|
+
'Read text from stdin instead of the positional argument',
|
|
666
|
+
)
|
|
667
|
+
.option(
|
|
668
|
+
'--json',
|
|
669
|
+
'Output result metadata as JSON to stdout (model, file path, cost)',
|
|
670
|
+
)
|
|
671
|
+
.option(
|
|
672
|
+
'--stdout',
|
|
673
|
+
'Write raw audio bytes to stdout instead of saving to a file. Useful for piping',
|
|
674
|
+
)
|
|
675
|
+
.example('# Generate speech with default model')
|
|
676
|
+
.example('egaki speech "Hello, welcome to egaki!"')
|
|
677
|
+
.example('# Use a specific model and voice')
|
|
678
|
+
.example('egaki speech "Good morning" -m tts-1-hd --voice nova')
|
|
679
|
+
.example('# Use GPT-4o Mini TTS with style instructions')
|
|
680
|
+
.example('egaki speech "Breaking news today" -m gpt-4o-mini-tts --instructions "Speak like a news anchor"')
|
|
681
|
+
.example('# ElevenLabs with a voice ID')
|
|
682
|
+
.example('egaki speech "Hello world" -m eleven_v3 --voice 21m00Tcm4TlvDq8ikWAM')
|
|
683
|
+
.example('# Read text from stdin')
|
|
684
|
+
.example('cat script.txt | egaki speech --stdin -o narration.mp3')
|
|
685
|
+
.example('# Cartesia Sonic 3.5 with a voice ID')
|
|
686
|
+
.example('egaki speech "Hello world" -m sonic-3.5 --voice f786b574-daa5-4673-aa0c-cbe3e8534c02')
|
|
687
|
+
.example('# Pipe audio to another tool')
|
|
688
|
+
.example('egaki speech "test" --stdout | ffplay -nodisp -autoexit -')
|
|
689
|
+
.action(async (text = '', options) => {
|
|
690
|
+
let inputText = text
|
|
691
|
+
if (options.stdin) {
|
|
692
|
+
inputText = await readTextFromStdin()
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!inputText || inputText.trim().length === 0) {
|
|
696
|
+
console.error(pc.red('No text provided. Pass text as an argument or use --stdin.'))
|
|
697
|
+
process.exit(1)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const model = options.model ?? await resolveSpeechModel()
|
|
701
|
+
const outputPath = options.output ?? 'egaki-output'
|
|
702
|
+
|
|
703
|
+
if (!options.stdout) {
|
|
704
|
+
console.error(pc.dim(`Model: ${model}`))
|
|
705
|
+
console.error(pc.dim(`Text: ${inputText.length > 80 ? inputText.slice(0, 80) + '...' : inputText}`))
|
|
706
|
+
console.error(pc.cyan('Generating speech...'))
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const result = await runProviderCall('Speech generation', () => generateSpeech({
|
|
710
|
+
text: inputText,
|
|
711
|
+
model,
|
|
712
|
+
voice: options.voice,
|
|
713
|
+
outputFormat: options.outputFormat,
|
|
714
|
+
instructions: options.instructions,
|
|
715
|
+
speed: options.speed,
|
|
716
|
+
language: options.language,
|
|
717
|
+
}))
|
|
718
|
+
|
|
719
|
+
if (options.stdout) {
|
|
720
|
+
process.stdout.write(Buffer.from(result.audio.uint8Array))
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const ext = extensionFromMediaType(result.audio.mediaType)
|
|
725
|
+
const finalPath = ensureExtension(outputPath, ext)
|
|
726
|
+
fs.writeFileSync(finalPath, result.audio.uint8Array)
|
|
727
|
+
console.error(pc.green(`Saved: ${finalPath}`))
|
|
728
|
+
printCost(result.cost)
|
|
729
|
+
|
|
730
|
+
if (options.json) {
|
|
731
|
+
console.log(JSON.stringify({
|
|
732
|
+
model,
|
|
733
|
+
file: finalPath,
|
|
734
|
+
cost: result.cost,
|
|
735
|
+
textLength: inputText.length,
|
|
736
|
+
timestamps: result.timestamps ? result.timestamps.length : 0,
|
|
737
|
+
}, null, 2))
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// ─── transcribe command ──────────────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
cli
|
|
744
|
+
.command(
|
|
745
|
+
'transcribe [audio]',
|
|
746
|
+
dedent`
|
|
747
|
+
Transcribe audio to text using AI speech-to-text models.
|
|
748
|
+
Supports OpenAI (whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe),
|
|
749
|
+
ElevenLabs (scribe_v1), Deepgram (nova-3), Groq (whisper-large-v3,
|
|
750
|
+
whisper-large-v3-turbo, distil-whisper-large-v3-en), and Cartesia
|
|
751
|
+
(ink-whisper). Outputs JSON with text, timestamps, language, and duration.
|
|
752
|
+
`,
|
|
753
|
+
)
|
|
754
|
+
.option(
|
|
755
|
+
'-m, --model [model]',
|
|
756
|
+
z.string().describe('Transcription model ID. If omitted, shows an interactive picker (or uses default in non-TTY mode)'),
|
|
757
|
+
)
|
|
758
|
+
.option(
|
|
759
|
+
'-o, --output [path]',
|
|
760
|
+
z
|
|
761
|
+
.string()
|
|
762
|
+
.describe('Output file path for the JSON result'),
|
|
763
|
+
)
|
|
764
|
+
.option(
|
|
765
|
+
'--language [lang]',
|
|
766
|
+
z
|
|
767
|
+
.string()
|
|
768
|
+
.describe('ISO 639-1 language hint (e.g. en, es, fr). Helps accuracy for known languages'),
|
|
769
|
+
)
|
|
770
|
+
.option(
|
|
771
|
+
'--stdin',
|
|
772
|
+
'Read audio from stdin instead of a file path',
|
|
773
|
+
)
|
|
774
|
+
.option(
|
|
775
|
+
'--stdout',
|
|
776
|
+
'Write only the plain text transcript to stdout (no JSON metadata)',
|
|
777
|
+
)
|
|
778
|
+
.example('# Transcribe an audio file with default model')
|
|
779
|
+
.example('egaki transcribe recording.mp3')
|
|
780
|
+
.example('# Use a specific model')
|
|
781
|
+
.example('egaki transcribe interview.wav -m gpt-4o-transcribe')
|
|
782
|
+
.example('# Groq for fast, cheap transcription')
|
|
783
|
+
.example('egaki transcribe podcast.mp3 -m whisper-large-v3')
|
|
784
|
+
.example('# Get plain text only')
|
|
785
|
+
.example('egaki transcribe recording.mp3 --stdout')
|
|
786
|
+
.example('# Save JSON result to file')
|
|
787
|
+
.example('egaki transcribe recording.mp3 -o transcript.json')
|
|
788
|
+
.example('# Pipe audio from another command')
|
|
789
|
+
.example('ffmpeg -i video.mp4 -f mp3 - | egaki transcribe --stdin -m whisper-1')
|
|
790
|
+
.action(async (audioPath = '', options) => {
|
|
791
|
+
if (!options.stdin && !audioPath) {
|
|
792
|
+
console.error(pc.red('No audio provided. Pass an audio file path or use --stdin.'))
|
|
793
|
+
process.exit(1)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
let audioData: Uint8Array
|
|
797
|
+
|
|
798
|
+
if (options.stdin) {
|
|
799
|
+
const chunks: Buffer[] = []
|
|
800
|
+
for await (const chunk of process.stdin) {
|
|
801
|
+
chunks.push(chunk)
|
|
802
|
+
}
|
|
803
|
+
audioData = new Uint8Array(Buffer.concat(chunks))
|
|
804
|
+
} else {
|
|
805
|
+
const resolved = path.resolve(audioPath)
|
|
806
|
+
if (!fs.existsSync(resolved)) {
|
|
807
|
+
console.error(pc.red(`File not found: ${resolved}`))
|
|
808
|
+
process.exit(1)
|
|
809
|
+
}
|
|
810
|
+
audioData = new Uint8Array(fs.readFileSync(resolved))
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (audioData.length === 0) {
|
|
814
|
+
console.error(pc.red('No audio data provided. Pass an audio file path or use --stdin.'))
|
|
815
|
+
process.exit(1)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const model = options.model ?? await resolveTranscriptionModel()
|
|
819
|
+
|
|
820
|
+
if (!options.stdout) {
|
|
821
|
+
console.error(pc.dim(`Model: ${model}`))
|
|
822
|
+
console.error(pc.dim(`Audio: ${options.stdin ? 'stdin' : audioPath} (${(audioData.length / 1024).toFixed(1)} KB)`))
|
|
823
|
+
console.error(pc.cyan('Transcribing...'))
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const result = await runProviderCall('Transcription', () => transcribeAudio({
|
|
827
|
+
audio: audioData,
|
|
828
|
+
model,
|
|
829
|
+
language: options.language,
|
|
830
|
+
}))
|
|
831
|
+
|
|
832
|
+
if (options.stdout) {
|
|
833
|
+
process.stdout.write(result.text)
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const output = {
|
|
838
|
+
text: result.text,
|
|
839
|
+
segments: result.segments,
|
|
840
|
+
wordTimestamps: segmentsToWordTimestamps(result.segments),
|
|
841
|
+
language: result.language,
|
|
842
|
+
durationInSeconds: result.durationInSeconds,
|
|
843
|
+
model: result.model,
|
|
844
|
+
cost: result.cost,
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (options.output) {
|
|
848
|
+
const outputPath = path.resolve(options.output)
|
|
849
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2))
|
|
850
|
+
console.error(pc.green(`Saved: ${outputPath}`))
|
|
851
|
+
} else {
|
|
852
|
+
console.log(JSON.stringify(output, null, 2))
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
printCost(result.cost)
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
// ─── demucs command ──────────────────────────────────────────────────────────
|
|
859
|
+
|
|
860
|
+
cli
|
|
861
|
+
.command(
|
|
862
|
+
'demucs [audio]',
|
|
863
|
+
dedent`
|
|
864
|
+
Separate audio into stems (vocals, background, drums, bass, etc.)
|
|
865
|
+
using fal.ai's Demucs model. Requires a fal.ai API key.
|
|
866
|
+
By default extracts vocals and background (other) stems.
|
|
867
|
+
`,
|
|
868
|
+
)
|
|
869
|
+
.option(
|
|
870
|
+
'--stems [list]',
|
|
871
|
+
z
|
|
872
|
+
.string()
|
|
873
|
+
.describe('Comma-separated stems to extract: vocals, drums, bass, other, guitar, piano. Default: vocals,other'),
|
|
874
|
+
)
|
|
875
|
+
.option(
|
|
876
|
+
'--model [model]',
|
|
877
|
+
z
|
|
878
|
+
.string()
|
|
879
|
+
.describe('Demucs model variant. Default: htdemucs_6s. Options: htdemucs, htdemucs_ft, htdemucs_6s, hdemucs_mmi, mdx, mdx_extra, mdx_q, mdx_extra_q'),
|
|
880
|
+
)
|
|
881
|
+
.option(
|
|
882
|
+
'--output-format [format]',
|
|
883
|
+
z
|
|
884
|
+
.enum(['mp3', 'wav'])
|
|
885
|
+
.default('mp3')
|
|
886
|
+
.describe('Output audio format: mp3 or wav'),
|
|
887
|
+
)
|
|
888
|
+
.option(
|
|
889
|
+
'-o, --output [dir]',
|
|
890
|
+
z
|
|
891
|
+
.string()
|
|
892
|
+
.describe('Output directory for separated stems. Default: current directory'),
|
|
893
|
+
)
|
|
894
|
+
.option(
|
|
895
|
+
'--shifts [n]',
|
|
896
|
+
z
|
|
897
|
+
.number()
|
|
898
|
+
.describe('Number of random shifts for quality (higher = better but slower). Default: 1'),
|
|
899
|
+
)
|
|
900
|
+
.option(
|
|
901
|
+
'--overlap [n]',
|
|
902
|
+
z
|
|
903
|
+
.number()
|
|
904
|
+
.describe('Segment overlap 0.0-1.0 (higher = better quality). Default: 0.25'),
|
|
905
|
+
)
|
|
906
|
+
.option(
|
|
907
|
+
'--stdin',
|
|
908
|
+
'Read audio from stdin instead of a file path',
|
|
909
|
+
)
|
|
910
|
+
.option(
|
|
911
|
+
'--json',
|
|
912
|
+
'Output result metadata as JSON to stdout',
|
|
913
|
+
)
|
|
914
|
+
.example('# Separate vocals and background from an audio file')
|
|
915
|
+
.example('egaki demucs song.mp3')
|
|
916
|
+
.example('# Extract all stems (vocals, drums, bass, guitar, piano, other)')
|
|
917
|
+
.example('egaki demucs song.mp3 --stems vocals,drums,bass,other,guitar,piano')
|
|
918
|
+
.example('# Extract only vocals as WAV')
|
|
919
|
+
.example('egaki demucs song.mp3 --stems vocals --output-format wav')
|
|
920
|
+
.example('# Save to a specific directory')
|
|
921
|
+
.example('egaki demucs song.mp3 -o stems/')
|
|
922
|
+
.example('# Higher quality (slower)')
|
|
923
|
+
.example('egaki demucs song.mp3 --shifts 5')
|
|
924
|
+
.action(async (audioPath = '', options) => {
|
|
925
|
+
if (!options.stdin && !audioPath) {
|
|
926
|
+
console.error(pc.red('No audio provided. Pass an audio file path or use --stdin.'))
|
|
927
|
+
process.exit(1)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
let audioData: Uint8Array
|
|
931
|
+
let filename: string | undefined
|
|
932
|
+
|
|
933
|
+
if (options.stdin) {
|
|
934
|
+
const chunks: Buffer[] = []
|
|
935
|
+
for await (const chunk of process.stdin) {
|
|
936
|
+
chunks.push(chunk)
|
|
937
|
+
}
|
|
938
|
+
audioData = new Uint8Array(Buffer.concat(chunks))
|
|
939
|
+
} else {
|
|
940
|
+
const resolved = path.resolve(audioPath)
|
|
941
|
+
if (!fs.existsSync(resolved)) {
|
|
942
|
+
console.error(pc.red(`File not found: ${resolved}`))
|
|
943
|
+
process.exit(1)
|
|
944
|
+
}
|
|
945
|
+
audioData = new Uint8Array(fs.readFileSync(resolved))
|
|
946
|
+
filename = path.basename(resolved)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (audioData.length === 0) {
|
|
950
|
+
console.error(pc.red('No audio data provided.'))
|
|
951
|
+
process.exit(1)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const stems: StemName[] = options.stems
|
|
955
|
+
? (options.stems as string).split(',').map((s: string) => s.trim() as StemName)
|
|
956
|
+
: ['vocals', 'other']
|
|
957
|
+
const model = (options.model ?? 'htdemucs_6s') as DemucsModel
|
|
958
|
+
const outputFormat = options.outputFormat ?? 'mp3'
|
|
959
|
+
const outputDir = options.output ? path.resolve(options.output) : process.cwd()
|
|
960
|
+
|
|
961
|
+
console.error(pc.dim(`Model: ${model}`))
|
|
962
|
+
console.error(pc.dim(`Stems: ${stems.join(', ')}`))
|
|
963
|
+
console.error(pc.dim(`Audio: ${options.stdin ? 'stdin' : audioPath} (${(audioData.length / 1024 / 1024).toFixed(1)} MB)`))
|
|
964
|
+
console.error(pc.cyan('Separating audio...'))
|
|
965
|
+
|
|
966
|
+
const result = await runProviderCall('Audio separation', () => separateAudio({
|
|
967
|
+
audio: audioData,
|
|
968
|
+
stems,
|
|
969
|
+
model,
|
|
970
|
+
outputFormat,
|
|
971
|
+
shifts: options.shifts,
|
|
972
|
+
overlap: options.overlap,
|
|
973
|
+
filename,
|
|
974
|
+
onProgress: (msg) => console.error(pc.dim(msg)),
|
|
975
|
+
}))
|
|
976
|
+
|
|
977
|
+
// Create output directory if needed
|
|
978
|
+
if (!fs.existsSync(outputDir)) {
|
|
979
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Download and save each stem
|
|
983
|
+
const baseName = filename
|
|
984
|
+
? path.basename(filename, path.extname(filename))
|
|
985
|
+
: 'audio'
|
|
986
|
+
const savedFiles: Array<{ stem: string; path: string; size: number }> = []
|
|
987
|
+
|
|
988
|
+
for (const stem of result.stems) {
|
|
989
|
+
console.error(pc.dim(`Downloading ${stem.name}...`))
|
|
990
|
+
const bytes = await downloadStem(stem.url)
|
|
991
|
+
if (bytes instanceof Error) {
|
|
992
|
+
console.error(pc.red(`Failed to download ${stem.name}: ${bytes.message}`))
|
|
993
|
+
continue
|
|
994
|
+
}
|
|
995
|
+
const stemPath = path.join(outputDir, `${baseName}-${stem.name}.${outputFormat}`)
|
|
996
|
+
fs.writeFileSync(stemPath, bytes)
|
|
997
|
+
savedFiles.push({ stem: stem.name, path: stemPath, size: bytes.length })
|
|
998
|
+
console.error(pc.green(`Saved: ${stemPath} (${(bytes.length / 1024 / 1024).toFixed(1)} MB)`))
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (options.json) {
|
|
1002
|
+
console.log(JSON.stringify({
|
|
1003
|
+
model: result.model,
|
|
1004
|
+
stems: savedFiles,
|
|
1005
|
+
}, null, 2))
|
|
1006
|
+
}
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
// ─── voice clone command ─────────────────────────────────────────────────────
|
|
1010
|
+
|
|
1011
|
+
cli
|
|
1012
|
+
.command(
|
|
1013
|
+
'voice clone [audio]',
|
|
1014
|
+
dedent`
|
|
1015
|
+
Clone a voice from an audio clip. Returns a voice ID for use with
|
|
1016
|
+
'egaki speech --voice <id>'. Supports Cartesia (default) and ElevenLabs.
|
|
1017
|
+
|
|
1018
|
+
Best practices for high-quality clones:
|
|
1019
|
+
|
|
1020
|
+
1. Isolate vocals first: 'egaki demucs recording.mp3 --stems vocals'
|
|
1021
|
+
removes background music, noise, and other speakers.
|
|
1022
|
+
2. Find a clean snippet: 'egaki transcribe recording-vocals.mp3' to get
|
|
1023
|
+
word timestamps. Pick a 5-10s segment with a complete phrase, clear
|
|
1024
|
+
speech, no hesitations or crosstalk.
|
|
1025
|
+
3. Trim to speech boundaries:
|
|
1026
|
+
ffmpeg -i recording-vocals.mp3 -ss 12.5 -to 22.0 -c copy clip.mp3
|
|
1027
|
+
No silence padding at start or end.
|
|
1028
|
+
4. Match energy to intent: the clone mimics the tone and pacing of the
|
|
1029
|
+
source clip. Use an energetic clip for energetic output.
|
|
1030
|
+
5. Speak in the target language. Use --language for Cartesia clones.
|
|
1031
|
+
|
|
1032
|
+
Cartesia: up to 10s of audio, instant, free. Good for short clean clips.
|
|
1033
|
+
ElevenLabs: 1-3 min recommended, has --remove-background-noise option.
|
|
1034
|
+
`,
|
|
1035
|
+
)
|
|
1036
|
+
.option(
|
|
1037
|
+
'--name [name]',
|
|
1038
|
+
z
|
|
1039
|
+
.string()
|
|
1040
|
+
.describe('Name for the cloned voice (required)'),
|
|
1041
|
+
)
|
|
1042
|
+
.option(
|
|
1043
|
+
'-p, --provider [provider]',
|
|
1044
|
+
z
|
|
1045
|
+
.enum(['cartesia', 'elevenlabs'])
|
|
1046
|
+
.default('cartesia')
|
|
1047
|
+
.describe('Voice cloning provider: cartesia or elevenlabs'),
|
|
1048
|
+
)
|
|
1049
|
+
.option(
|
|
1050
|
+
'--language [lang]',
|
|
1051
|
+
z
|
|
1052
|
+
.string()
|
|
1053
|
+
.describe('Cartesia only: ISO 639-1 language code (default: en). E.g. en, es, fr, de, ja'),
|
|
1054
|
+
)
|
|
1055
|
+
.option(
|
|
1056
|
+
'--description [text]',
|
|
1057
|
+
z
|
|
1058
|
+
.string()
|
|
1059
|
+
.describe('Optional description for the voice'),
|
|
1060
|
+
)
|
|
1061
|
+
.option(
|
|
1062
|
+
'--base-voice-id [id]',
|
|
1063
|
+
z
|
|
1064
|
+
.string()
|
|
1065
|
+
.describe('Cartesia: optional base voice ID to derive from'),
|
|
1066
|
+
)
|
|
1067
|
+
.option(
|
|
1068
|
+
'--remove-background-noise',
|
|
1069
|
+
'ElevenLabs: apply AI noise removal to the clip before cloning',
|
|
1070
|
+
)
|
|
1071
|
+
.option(
|
|
1072
|
+
'--stdin',
|
|
1073
|
+
'Read audio from stdin instead of a file path',
|
|
1074
|
+
)
|
|
1075
|
+
.option(
|
|
1076
|
+
'--json',
|
|
1077
|
+
'Output result as JSON to stdout',
|
|
1078
|
+
)
|
|
1079
|
+
.example('# Clone a voice from a recording')
|
|
1080
|
+
.example('egaki voice clone recording.wav --name "My Voice"')
|
|
1081
|
+
.example('# Clone with ElevenLabs and noise removal')
|
|
1082
|
+
.example('egaki voice clone vocals.mp3 --name "Narrator" --provider elevenlabs --remove-background-noise')
|
|
1083
|
+
.example('# Full pipeline: separate → trim → clone')
|
|
1084
|
+
.example('egaki demucs interview.mp3 --stems vocals')
|
|
1085
|
+
.example('ffmpeg -i interview-vocals.mp3 -ss 5.0 -to 15.0 -c copy clip.mp3')
|
|
1086
|
+
.example('egaki voice clone clip.mp3 --name "Speaker"')
|
|
1087
|
+
.example('# Use the cloned voice')
|
|
1088
|
+
.example('egaki speech "Hello world" --voice <voice-id>')
|
|
1089
|
+
.action(async (audioPath = '', options) => {
|
|
1090
|
+
if (!options.stdin && !audioPath) {
|
|
1091
|
+
console.error(pc.red('No audio provided. Pass an audio file path or use --stdin.'))
|
|
1092
|
+
process.exit(1)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const name = options.name
|
|
1096
|
+
if (!name) {
|
|
1097
|
+
console.error(pc.red('Voice name is required. Use --name "My Voice".'))
|
|
1098
|
+
process.exit(1)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
let audioData: Uint8Array
|
|
1102
|
+
let filename: string | undefined
|
|
1103
|
+
|
|
1104
|
+
if (options.stdin) {
|
|
1105
|
+
const chunks: Buffer[] = []
|
|
1106
|
+
for await (const chunk of process.stdin) {
|
|
1107
|
+
chunks.push(chunk)
|
|
1108
|
+
}
|
|
1109
|
+
audioData = new Uint8Array(Buffer.concat(chunks))
|
|
1110
|
+
} else {
|
|
1111
|
+
const resolved = path.resolve(audioPath)
|
|
1112
|
+
if (!fs.existsSync(resolved)) {
|
|
1113
|
+
console.error(pc.red(`File not found: ${resolved}`))
|
|
1114
|
+
process.exit(1)
|
|
1115
|
+
}
|
|
1116
|
+
audioData = new Uint8Array(fs.readFileSync(resolved))
|
|
1117
|
+
filename = path.basename(resolved)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (audioData.length === 0) {
|
|
1121
|
+
console.error(pc.red('No audio data provided.'))
|
|
1122
|
+
process.exit(1)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const provider = (options.provider ?? 'cartesia') as CloneProvider
|
|
1126
|
+
|
|
1127
|
+
console.error(pc.dim(`Provider: ${provider}`))
|
|
1128
|
+
console.error(pc.dim(`Name: ${name}`))
|
|
1129
|
+
console.error(pc.dim(`Audio: ${options.stdin ? 'stdin' : audioPath} (${(audioData.length / 1024).toFixed(1)} KB)`))
|
|
1130
|
+
console.error(pc.cyan('Cloning voice...'))
|
|
1131
|
+
|
|
1132
|
+
const result = await runProviderCall('Voice cloning', () => cloneVoice({
|
|
1133
|
+
audio: audioData,
|
|
1134
|
+
name,
|
|
1135
|
+
provider,
|
|
1136
|
+
language: options.language,
|
|
1137
|
+
description: options.description,
|
|
1138
|
+
baseVoiceId: options.baseVoiceId,
|
|
1139
|
+
removeBackgroundNoise: options.removeBackgroundNoise || false,
|
|
1140
|
+
filename,
|
|
1141
|
+
}))
|
|
1142
|
+
|
|
1143
|
+
console.error(pc.green(`Voice cloned successfully!`))
|
|
1144
|
+
console.error(pc.bold(`Voice ID: ${result.voiceId}`))
|
|
1145
|
+
console.error(pc.dim(`Use it: egaki speech "Hello" --voice ${result.voiceId}`))
|
|
1146
|
+
|
|
1147
|
+
if (options.json) {
|
|
1148
|
+
console.log(JSON.stringify({
|
|
1149
|
+
voiceId: result.voiceId,
|
|
1150
|
+
name: result.name,
|
|
1151
|
+
provider: result.provider,
|
|
1152
|
+
}, null, 2))
|
|
1153
|
+
}
|
|
604
1154
|
})
|
|
605
1155
|
|
|
606
1156
|
// ─── models command ──────────────────────────────────────────────────────────
|
|
@@ -620,9 +1170,9 @@ cli
|
|
|
620
1170
|
.option(
|
|
621
1171
|
'--type [type]',
|
|
622
1172
|
z
|
|
623
|
-
.enum(['all', 'image', 'video'])
|
|
1173
|
+
.enum(['all', 'image', 'video', 'speech', 'transcription'])
|
|
624
1174
|
.default('all')
|
|
625
|
-
.describe('Filter by model type: image
|
|
1175
|
+
.describe('Filter by model type: image, video, speech, transcription, or all'),
|
|
626
1176
|
)
|
|
627
1177
|
.option('--json', 'Output as JSON instead of YAML')
|
|
628
1178
|
.action(async (options) => {
|
|
@@ -631,12 +1181,16 @@ cli
|
|
|
631
1181
|
Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]),
|
|
632
1182
|
)
|
|
633
1183
|
|
|
634
|
-
let models: Array<typeof CATALOG[number] | typeof VIDEO_CATALOG[number]> =
|
|
1184
|
+
let models: Array<typeof CATALOG[number] | typeof VIDEO_CATALOG[number] | SpeechModelEntry | TranscriptionModelEntry> =
|
|
635
1185
|
options.type === 'video'
|
|
636
1186
|
? [...VIDEO_CATALOG]
|
|
637
1187
|
: options.type === 'image'
|
|
638
1188
|
? [...CATALOG]
|
|
639
|
-
:
|
|
1189
|
+
: options.type === 'speech'
|
|
1190
|
+
? [...SPEECH_CATALOG]
|
|
1191
|
+
: options.type === 'transcription'
|
|
1192
|
+
? [...TRANSCRIPTION_CATALOG]
|
|
1193
|
+
: [...CATALOG, ...VIDEO_CATALOG, ...SPEECH_CATALOG, ...TRANSCRIPTION_CATALOG]
|
|
640
1194
|
|
|
641
1195
|
if (options.provider) {
|
|
642
1196
|
models = models.filter((m) => m.provider === options.provider)
|
|
@@ -669,14 +1223,34 @@ cli
|
|
|
669
1223
|
? `${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}`
|
|
670
1224
|
: 'unknown',
|
|
671
1225
|
}
|
|
672
|
-
:
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1226
|
+
: m.strategy === 'speech'
|
|
1227
|
+
? {
|
|
1228
|
+
outputFormats: m.features.outputFormats.join(', '),
|
|
1229
|
+
instructions: m.features.instructions,
|
|
1230
|
+
speed: m.features.speed,
|
|
1231
|
+
language: m.features.language,
|
|
1232
|
+
maxChars: m.features.maxChars || 'unlimited',
|
|
1233
|
+
voices: m.features.defaultVoices.length > 0
|
|
1234
|
+
? m.features.defaultVoices.join(', ')
|
|
1235
|
+
: 'use provider voice library',
|
|
1236
|
+
}
|
|
1237
|
+
: m.strategy === 'transcription'
|
|
1238
|
+
? {
|
|
1239
|
+
wordTimestamps: m.features.wordTimestamps,
|
|
1240
|
+
diarization: m.features.diarization,
|
|
1241
|
+
languageDetection: m.features.languageDetection,
|
|
1242
|
+
languageHint: m.features.languageHint,
|
|
1243
|
+
inputFormats: m.features.inputFormats.join(', '),
|
|
1244
|
+
maxDurationSec: m.features.maxDurationSec || 'unlimited',
|
|
1245
|
+
}
|
|
1246
|
+
: {
|
|
1247
|
+
editing: m.features.editing,
|
|
1248
|
+
inpainting: m.features.inpainting,
|
|
1249
|
+
seed: m.features.seed,
|
|
1250
|
+
multipleImages: m.features.multipleImages,
|
|
1251
|
+
aspectRatios: m.features.aspectRatios.join(', ') || 'none',
|
|
1252
|
+
...(m.features.sizes ? { sizes: m.features.sizes.join(', ') } : {}),
|
|
1253
|
+
},
|
|
680
1254
|
}))
|
|
681
1255
|
|
|
682
1256
|
if (options.json) {
|
|
@@ -693,18 +1267,30 @@ const MAX_UPLOAD_SIZE = 100 * 1024 * 1024
|
|
|
693
1267
|
|
|
694
1268
|
cli.help()
|
|
695
1269
|
cli.version(pkg.version)
|
|
696
|
-
|
|
1270
|
+
|
|
1271
|
+
export { cli }
|
|
1272
|
+
|
|
697
1273
|
|
|
698
1274
|
// ─── error output helpers ────────────────────────────────────────────────────
|
|
699
1275
|
|
|
700
1276
|
type PrintableValue = unknown
|
|
701
1277
|
type ErrorDetailsInput = Parameters<typeof APICallError.isInstance>[0]
|
|
702
1278
|
|
|
703
|
-
async function runProviderCall<T>(operation: string, call: () => Promise<T>): Promise<T> {
|
|
1279
|
+
async function runProviderCall<T>(operation: string, call: () => Promise<Error | T>): Promise<T> {
|
|
704
1280
|
const result = await call().catch((error) => {
|
|
705
1281
|
printErrorDetails(error, operation)
|
|
706
1282
|
process.exit(1)
|
|
707
1283
|
})
|
|
1284
|
+
if (result instanceof Error) {
|
|
1285
|
+
// Validation errors get a clean one-line message; provider/runtime errors
|
|
1286
|
+
// get full details (status code, response body, stack trace, etc.).
|
|
1287
|
+
if (result instanceof ValidationError) {
|
|
1288
|
+
console.error(pc.red(result.message))
|
|
1289
|
+
process.exit(1)
|
|
1290
|
+
}
|
|
1291
|
+
printErrorDetails(result, operation)
|
|
1292
|
+
process.exit(1)
|
|
1293
|
+
}
|
|
708
1294
|
return result
|
|
709
1295
|
}
|
|
710
1296
|
|
|
@@ -749,6 +1335,18 @@ function printErrorDetails(error: ErrorDetailsInput, operation?: string): void {
|
|
|
749
1335
|
if (error instanceof Error) {
|
|
750
1336
|
printKeyValue('name', error.name)
|
|
751
1337
|
printKeyValue('message', error.message)
|
|
1338
|
+
// @ai-sdk/xai discards statusResponse.error when status==='failed',
|
|
1339
|
+
// so XAI_VIDEO_GENERATION_FAILED carries no useful info. Add a hint.
|
|
1340
|
+
if (error.name === 'XAI_VIDEO_GENERATION_FAILED') {
|
|
1341
|
+
console.error(
|
|
1342
|
+
pc.dim(
|
|
1343
|
+
'hint: xAI returned status "failed" but @ai-sdk/xai discards the error details.\n' +
|
|
1344
|
+
'Common causes: invalid/corrupt input image, content policy violation, or transient backend error.\n' +
|
|
1345
|
+
'To see the real error, poll the video status endpoint manually:\n' +
|
|
1346
|
+
' curl https://api.x.ai/v1/videos/<request_id> -H "Authorization: Bearer $TOKEN"',
|
|
1347
|
+
),
|
|
1348
|
+
)
|
|
1349
|
+
}
|
|
752
1350
|
printJsonBlock('details', serializeOwnProperties(error))
|
|
753
1351
|
printCause(error.cause)
|
|
754
1352
|
printStack(error)
|
|
@@ -841,7 +1439,7 @@ const FEATURED_IMAGE_MODELS = [
|
|
|
841
1439
|
'flux-pro-1.1-ultra',
|
|
842
1440
|
'recraft-v4',
|
|
843
1441
|
'dall-e-3',
|
|
844
|
-
'grok-imagine-image-
|
|
1442
|
+
'grok-imagine-image-quality',
|
|
845
1443
|
]
|
|
846
1444
|
|
|
847
1445
|
/**
|
|
@@ -911,6 +1509,75 @@ async function resolveVideoModel(): Promise<string> {
|
|
|
911
1509
|
return selected
|
|
912
1510
|
}
|
|
913
1511
|
|
|
1512
|
+
/**
|
|
1513
|
+
* Resolve the speech model: show interactive picker in TTY, use default otherwise.
|
|
1514
|
+
*/
|
|
1515
|
+
async function resolveSpeechModel(): Promise<string> {
|
|
1516
|
+
const isTTY = process.stdin.isTTY && process.stderr.isTTY
|
|
1517
|
+
if (!isTTY) return DEFAULT_SPEECH_MODEL
|
|
1518
|
+
|
|
1519
|
+
const options = SPEECH_CATALOG.map((m) => {
|
|
1520
|
+
const cost = pc.dim(`$${m.cost.perMillionChars}/M chars`)
|
|
1521
|
+
return {
|
|
1522
|
+
value: m.id,
|
|
1523
|
+
label: `${m.name} ${cost}`,
|
|
1524
|
+
hint: m.id,
|
|
1525
|
+
}
|
|
1526
|
+
})
|
|
1527
|
+
|
|
1528
|
+
const selected = await select({
|
|
1529
|
+
message: 'Select a speech model',
|
|
1530
|
+
options,
|
|
1531
|
+
})
|
|
1532
|
+
|
|
1533
|
+
if (isCancel(selected)) {
|
|
1534
|
+
cancel('Cancelled.')
|
|
1535
|
+
process.exit(0)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return selected
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Resolve the transcription model: show interactive picker in TTY, use default otherwise.
|
|
1543
|
+
*/
|
|
1544
|
+
async function resolveTranscriptionModel(): Promise<string> {
|
|
1545
|
+
const isTTY = process.stdin.isTTY && process.stderr.isTTY
|
|
1546
|
+
if (!isTTY) return DEFAULT_TRANSCRIPTION_MODEL
|
|
1547
|
+
|
|
1548
|
+
const options = TRANSCRIPTION_CATALOG.map((m) => {
|
|
1549
|
+
const cost = pc.dim(`${formatCost(m.cost.perSecond * 60)}/min`)
|
|
1550
|
+
return {
|
|
1551
|
+
value: m.id,
|
|
1552
|
+
label: `${m.name} ${cost}`,
|
|
1553
|
+
hint: m.id,
|
|
1554
|
+
}
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
const selected = await select({
|
|
1558
|
+
message: 'Select a transcription model',
|
|
1559
|
+
options,
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1562
|
+
if (isCancel(selected)) {
|
|
1563
|
+
cancel('Cancelled.')
|
|
1564
|
+
process.exit(0)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
return selected
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Read text from stdin (for piping: cat file.txt | egaki speech --stdin).
|
|
1572
|
+
*/
|
|
1573
|
+
async function readTextFromStdin(): Promise<string> {
|
|
1574
|
+
const chunks: Buffer[] = []
|
|
1575
|
+
for await (const chunk of process.stdin) {
|
|
1576
|
+
chunks.push(chunk)
|
|
1577
|
+
}
|
|
1578
|
+
return Buffer.concat(chunks).toString('utf-8').trim()
|
|
1579
|
+
}
|
|
1580
|
+
|
|
914
1581
|
function isUrl(input: string): boolean {
|
|
915
1582
|
return /^https?:\/\//i.test(input)
|
|
916
1583
|
}
|
|
@@ -926,6 +1593,13 @@ function mimeTypeFromExtension(ext: string): string {
|
|
|
926
1593
|
'.jpeg': 'image/jpeg',
|
|
927
1594
|
'.webp': 'image/webp',
|
|
928
1595
|
'.gif': 'image/gif',
|
|
1596
|
+
'.mp3': 'audio/mpeg',
|
|
1597
|
+
'.wav': 'audio/wav',
|
|
1598
|
+
'.pcm': 'audio/pcm',
|
|
1599
|
+
'.opus': 'audio/opus',
|
|
1600
|
+
'.aac': 'audio/aac',
|
|
1601
|
+
'.flac': 'audio/flac',
|
|
1602
|
+
'.ogg': 'audio/ogg',
|
|
929
1603
|
}
|
|
930
1604
|
return map[ext.toLowerCase()] || 'application/octet-stream'
|
|
931
1605
|
}
|
|
@@ -1007,639 +1681,8 @@ async function readInputImages(
|
|
|
1007
1681
|
return Promise.all(inputs.map((f) => readInputSource(f)))
|
|
1008
1682
|
}
|
|
1009
1683
|
|
|
1010
|
-
// ─── type-safe provider option builders ──────────────────────────────────────
|
|
1011
|
-
// Each builder switches on provider name and constructs a typed object using
|
|
1012
|
-
// `satisfies` with the SDK's exported type. This catches typos in keys and
|
|
1013
|
-
// invalid enum values at compile time instead of runtime.
|
|
1014
|
-
|
|
1015
|
-
type ImageOptionInputs = {
|
|
1016
|
-
aspectRatio?: `${number}:${number}`
|
|
1017
|
-
allowPeople: boolean
|
|
1018
|
-
quality?: string
|
|
1019
|
-
resolution?: string
|
|
1020
|
-
outputFormat?: string
|
|
1021
|
-
negativePrompt?: string
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Return type uses a mapped type that preserves the typed structure while
|
|
1025
|
-
// remaining compatible with the AI SDK's ProviderOptions (Record<string, JSONObject>).
|
|
1026
|
-
// The satisfies checks catch key typos and invalid enum values at compile time,
|
|
1027
|
-
// then we widen to the SDK's expected shape at the boundary.
|
|
1028
|
-
type ProviderOptionsResult = Record<string, Record<string, string | number | boolean | string[] | null | undefined | Record<string, string | number | boolean | null | undefined>>>
|
|
1029
|
-
|
|
1030
|
-
function buildImageProviderOptions(
|
|
1031
|
-
provider: string,
|
|
1032
|
-
opts: ImageOptionInputs,
|
|
1033
|
-
): ProviderOptionsResult {
|
|
1034
|
-
switch (provider) {
|
|
1035
|
-
case 'google': {
|
|
1036
|
-
const googleOpts = {
|
|
1037
|
-
...(opts.allowPeople ? { personGeneration: 'allow_all' as const } : {}),
|
|
1038
|
-
...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio as GoogleImageModelOptions['aspectRatio'] } : {}),
|
|
1039
|
-
} satisfies GoogleImageModelOptions
|
|
1040
|
-
return { google: googleOpts }
|
|
1041
|
-
}
|
|
1042
|
-
case 'vertex': {
|
|
1043
|
-
const vertexOpts = {
|
|
1044
|
-
...(opts.allowPeople ? { personGeneration: 'allow_all' as const } : {}),
|
|
1045
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
1046
|
-
} satisfies GoogleVertexImageModelOptions
|
|
1047
|
-
return { vertex: vertexOpts }
|
|
1048
|
-
}
|
|
1049
|
-
case 'xai': {
|
|
1050
|
-
const xaiOpts = {
|
|
1051
|
-
...(opts.quality ? { quality: opts.quality as XaiImageModelOptions['quality'] } : {}),
|
|
1052
|
-
...(opts.resolution ? { resolution: opts.resolution as XaiImageModelOptions['resolution'] } : {}),
|
|
1053
|
-
...(opts.outputFormat ? { output_format: opts.outputFormat } : {}),
|
|
1054
|
-
} satisfies XaiImageModelOptions
|
|
1055
|
-
return { xai: xaiOpts }
|
|
1056
|
-
}
|
|
1057
|
-
case 'openai': {
|
|
1058
|
-
const openaiOpts = {
|
|
1059
|
-
...(opts.quality ? { quality: opts.quality as OpenAIImageProviderOptions['quality'] } : {}),
|
|
1060
|
-
...(opts.outputFormat ? { outputFormat: opts.outputFormat as OpenAIImageProviderOptions['outputFormat'] } : {}),
|
|
1061
|
-
} satisfies OpenAIImageProviderOptions
|
|
1062
|
-
return { openai: openaiOpts }
|
|
1063
|
-
}
|
|
1064
|
-
case 'fal': {
|
|
1065
|
-
const falOpts = {
|
|
1066
|
-
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
1067
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
1068
|
-
} satisfies FalImageModelOptions
|
|
1069
|
-
return { fal: falOpts }
|
|
1070
|
-
}
|
|
1071
|
-
default:
|
|
1072
|
-
// Providers routed through the gateway (bfl, recraft, etc.) pass options
|
|
1073
|
-
// as-is since they don't have local SDK types.
|
|
1074
|
-
return {}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
type VideoOptionInputs = {
|
|
1079
|
-
resolution?: string
|
|
1080
|
-
mode?: string
|
|
1081
|
-
videoUrl?: string
|
|
1082
|
-
referenceImages?: string[]
|
|
1083
|
-
negativePrompt?: string
|
|
1084
|
-
model: string
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
function buildVideoProviderOptions(
|
|
1088
|
-
provider: string,
|
|
1089
|
-
opts: VideoOptionInputs,
|
|
1090
|
-
): ProviderOptionsResult | undefined {
|
|
1091
|
-
switch (provider) {
|
|
1092
|
-
case 'xai': {
|
|
1093
|
-
// xAI video modes are a discriminated union; build the correct variant.
|
|
1094
|
-
const base = {
|
|
1095
|
-
...(opts.resolution ? { resolution: opts.resolution as '480p' | '720p' } : {}),
|
|
1096
|
-
}
|
|
1097
|
-
if (opts.mode === 'edit-video' && opts.videoUrl) {
|
|
1098
|
-
const xaiOpts = { ...base, mode: 'edit-video' as const, videoUrl: opts.videoUrl } satisfies XaiVideoModelOptions
|
|
1099
|
-
return { xai: xaiOpts }
|
|
1100
|
-
}
|
|
1101
|
-
if (opts.mode === 'extend-video' && opts.videoUrl) {
|
|
1102
|
-
const xaiOpts = { ...base, mode: 'extend-video' as const, videoUrl: opts.videoUrl } satisfies XaiVideoModelOptions
|
|
1103
|
-
return { xai: xaiOpts }
|
|
1104
|
-
}
|
|
1105
|
-
if (opts.mode === 'reference-to-video' && opts.referenceImages) {
|
|
1106
|
-
const xaiOpts = { ...base, mode: 'reference-to-video' as const, referenceImageUrls: opts.referenceImages } satisfies XaiVideoModelOptions
|
|
1107
|
-
return { xai: xaiOpts }
|
|
1108
|
-
}
|
|
1109
|
-
// Default: text-to-video or image-to-video (no mode)
|
|
1110
|
-
if (Object.keys(base).length === 0) return undefined
|
|
1111
|
-
const xaiOpts = { ...base } satisfies XaiVideoModelOptions
|
|
1112
|
-
return { xai: xaiOpts }
|
|
1113
|
-
}
|
|
1114
|
-
case 'fal': {
|
|
1115
|
-
const falOpts = {
|
|
1116
|
-
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
1117
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
1118
|
-
} satisfies FalVideoModelOptions
|
|
1119
|
-
if (Object.keys(falOpts).length === 0) return undefined
|
|
1120
|
-
return { fal: falOpts }
|
|
1121
|
-
}
|
|
1122
|
-
case 'google': {
|
|
1123
|
-
const googleOpts = {
|
|
1124
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
1125
|
-
} satisfies GoogleVideoModelOptions
|
|
1126
|
-
if (Object.keys(googleOpts).length === 0) return undefined
|
|
1127
|
-
return { google: googleOpts }
|
|
1128
|
-
}
|
|
1129
|
-
case 'vertex': {
|
|
1130
|
-
const vertexOpts = {
|
|
1131
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
1132
|
-
} satisfies GoogleVertexVideoModelOptions
|
|
1133
|
-
if (Object.keys(vertexOpts).length === 0) return undefined
|
|
1134
|
-
return { vertex: vertexOpts }
|
|
1135
|
-
}
|
|
1136
|
-
default:
|
|
1137
|
-
return undefined
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// Generate using the dedicated generateImage API (Imagen models)
|
|
1142
|
-
async function generateWithImageModel({
|
|
1143
|
-
prompt,
|
|
1144
|
-
model,
|
|
1145
|
-
outputPath,
|
|
1146
|
-
count,
|
|
1147
|
-
aspectRatio,
|
|
1148
|
-
seed,
|
|
1149
|
-
inputImages,
|
|
1150
|
-
maskImage,
|
|
1151
|
-
allowPeople,
|
|
1152
|
-
quality,
|
|
1153
|
-
resolution,
|
|
1154
|
-
outputFormat,
|
|
1155
|
-
negativePrompt,
|
|
1156
|
-
json,
|
|
1157
|
-
stdout,
|
|
1158
|
-
}: {
|
|
1159
|
-
prompt: string
|
|
1160
|
-
model: string
|
|
1161
|
-
outputPath: string
|
|
1162
|
-
count: number
|
|
1163
|
-
aspectRatio?: `${number}:${number}`
|
|
1164
|
-
seed?: number
|
|
1165
|
-
inputImages: Uint8Array[]
|
|
1166
|
-
maskImage?: Uint8Array
|
|
1167
|
-
allowPeople: boolean
|
|
1168
|
-
quality?: string
|
|
1169
|
-
resolution?: string
|
|
1170
|
-
outputFormat?: string
|
|
1171
|
-
negativePrompt?: string
|
|
1172
|
-
json: boolean
|
|
1173
|
-
stdout: boolean
|
|
1174
|
-
}) {
|
|
1175
|
-
// Build the prompt: plain string or multimodal with reference images
|
|
1176
|
-
const imagePrompt = inputImages.length > 0
|
|
1177
|
-
? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
|
|
1178
|
-
: prompt
|
|
1179
|
-
|
|
1180
|
-
const config = getModelConfig(model)
|
|
1181
|
-
const imageModel = await createImageModel(model)
|
|
1182
|
-
|
|
1183
|
-
// Build type-safe provider options using the SDK types with `satisfies`.
|
|
1184
|
-
// Each provider has its own shape; we switch on provider to get compile-time safety.
|
|
1185
|
-
const providerOptions = buildImageProviderOptions(config.provider, {
|
|
1186
|
-
aspectRatio,
|
|
1187
|
-
allowPeople,
|
|
1188
|
-
quality,
|
|
1189
|
-
resolution,
|
|
1190
|
-
outputFormat,
|
|
1191
|
-
negativePrompt,
|
|
1192
|
-
})
|
|
1193
|
-
|
|
1194
|
-
if (!stdout) {
|
|
1195
|
-
console.error(pc.cyan('Generating...'))
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
const result = await runProviderCall('Image generation', () => aiGenerateImage({
|
|
1199
|
-
model: imageModel,
|
|
1200
|
-
prompt: imagePrompt,
|
|
1201
|
-
n: count,
|
|
1202
|
-
...(aspectRatio ? { aspectRatio } : {}),
|
|
1203
|
-
...(seed !== undefined ? { seed } : {}),
|
|
1204
|
-
providerOptions,
|
|
1205
|
-
}))
|
|
1206
|
-
|
|
1207
|
-
const files = result.images.map((img) => ({
|
|
1208
|
-
uint8Array: img.uint8Array,
|
|
1209
|
-
mediaType: img.mediaType,
|
|
1210
|
-
}))
|
|
1211
|
-
|
|
1212
|
-
if (stdout) {
|
|
1213
|
-
writeFirstToStdout(files)
|
|
1214
|
-
return
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const savedFiles = saveGeneratedFiles(files, outputPath)
|
|
1218
|
-
const cost = calculateCost(config.cost, result.usage, result.images.length)
|
|
1219
|
-
printCost(cost)
|
|
1220
|
-
|
|
1221
|
-
if (json) {
|
|
1222
|
-
console.log(JSON.stringify({
|
|
1223
|
-
model,
|
|
1224
|
-
files: savedFiles,
|
|
1225
|
-
count: result.images.length,
|
|
1226
|
-
cost,
|
|
1227
|
-
usage: result.usage,
|
|
1228
|
-
warnings: result.warnings,
|
|
1229
|
-
}, null, 2))
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// Generate using generateText with responseModalities (Gemini multimodal models)
|
|
1234
|
-
async function generateWithTextModel({
|
|
1235
|
-
prompt,
|
|
1236
|
-
model,
|
|
1237
|
-
outputPath,
|
|
1238
|
-
inputImages,
|
|
1239
|
-
imageSize,
|
|
1240
|
-
aspectRatio,
|
|
1241
|
-
json,
|
|
1242
|
-
stdout,
|
|
1243
|
-
}: {
|
|
1244
|
-
prompt: string
|
|
1245
|
-
model: string
|
|
1246
|
-
outputPath: string
|
|
1247
|
-
inputImages: Uint8Array[]
|
|
1248
|
-
imageSize?: '1K' | '2K' | '4K'
|
|
1249
|
-
aspectRatio?: string
|
|
1250
|
-
json: boolean
|
|
1251
|
-
stdout: boolean
|
|
1252
|
-
}) {
|
|
1253
|
-
const textModel = await createTextModel(model)
|
|
1254
|
-
const config = getModelConfig(model)
|
|
1255
|
-
|
|
1256
|
-
if (!stdout) {
|
|
1257
|
-
console.error(pc.cyan('Generating...'))
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Build messages: if we have input images, create a multimodal prompt
|
|
1261
|
-
const messages = inputImages.length > 0
|
|
1262
|
-
? [
|
|
1263
|
-
{
|
|
1264
|
-
role: 'user' as const,
|
|
1265
|
-
content: [
|
|
1266
|
-
{ type: 'text' as const, text: prompt },
|
|
1267
|
-
...inputImages.map((img) => ({
|
|
1268
|
-
type: 'image' as const,
|
|
1269
|
-
image: img,
|
|
1270
|
-
})),
|
|
1271
|
-
],
|
|
1272
|
-
},
|
|
1273
|
-
]
|
|
1274
|
-
: undefined
|
|
1275
|
-
|
|
1276
|
-
// Type-safe Google/Vertex language model options with image generation config.
|
|
1277
|
-
// aspectRatio is cast to the Google SDK's enum type since CLI input is a free string.
|
|
1278
|
-
type GoogleAspectRatio = NonNullable<NonNullable<GoogleLanguageModelOptions['imageConfig']>['aspectRatio']>
|
|
1279
|
-
const googleOpts = {
|
|
1280
|
-
responseModalities: ['TEXT', 'IMAGE'],
|
|
1281
|
-
...(imageSize || aspectRatio
|
|
1282
|
-
? {
|
|
1283
|
-
imageConfig: {
|
|
1284
|
-
...(imageSize ? { imageSize } : {}),
|
|
1285
|
-
...(aspectRatio ? { aspectRatio: aspectRatio as GoogleAspectRatio } : {}),
|
|
1286
|
-
},
|
|
1287
|
-
}
|
|
1288
|
-
: {}),
|
|
1289
|
-
} satisfies GoogleLanguageModelOptions
|
|
1290
|
-
|
|
1291
|
-
const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google'
|
|
1292
|
-
|
|
1293
|
-
const result = await runProviderCall('Text/image generation', () => generateText({
|
|
1294
|
-
model: textModel,
|
|
1295
|
-
...(messages ? { messages } : { prompt }),
|
|
1296
|
-
providerOptions: {
|
|
1297
|
-
[providerOptionsKey]: googleOpts,
|
|
1298
|
-
},
|
|
1299
|
-
}))
|
|
1300
|
-
|
|
1301
|
-
const imageFiles = result.files.filter((f) => f.mediaType.startsWith('image/'))
|
|
1302
|
-
|
|
1303
|
-
if (imageFiles.length === 0) {
|
|
1304
|
-
console.error(pc.red('No images generated.'))
|
|
1305
|
-
process.exit(1)
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (stdout) {
|
|
1309
|
-
writeFirstToStdout(imageFiles)
|
|
1310
|
-
return
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
const savedFiles = saveGeneratedFiles(imageFiles, outputPath)
|
|
1314
|
-
const cost = calculateCost(config.cost, result.usage, imageFiles.length)
|
|
1315
|
-
printCost(cost)
|
|
1316
|
-
|
|
1317
|
-
if (result.text && !json) {
|
|
1318
|
-
console.error(pc.dim(result.text))
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
if (json) {
|
|
1322
|
-
console.log(JSON.stringify({
|
|
1323
|
-
model,
|
|
1324
|
-
files: savedFiles,
|
|
1325
|
-
count: imageFiles.length,
|
|
1326
|
-
text: result.text || null,
|
|
1327
|
-
cost,
|
|
1328
|
-
usage: result.usage,
|
|
1329
|
-
}, null, 2))
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// Generate using the OpenAI Responses API with imageGeneration tool.
|
|
1334
|
-
// Used when auth comes from ChatGPT OAuth (no Image API scope).
|
|
1335
|
-
async function generateWithResponsesApi({
|
|
1336
|
-
prompt,
|
|
1337
|
-
model,
|
|
1338
|
-
outputPath,
|
|
1339
|
-
count,
|
|
1340
|
-
aspectRatio,
|
|
1341
|
-
seed,
|
|
1342
|
-
inputImages,
|
|
1343
|
-
maskImage,
|
|
1344
|
-
json,
|
|
1345
|
-
stdout,
|
|
1346
|
-
}: {
|
|
1347
|
-
prompt: string
|
|
1348
|
-
model: string
|
|
1349
|
-
outputPath: string
|
|
1350
|
-
count: number
|
|
1351
|
-
aspectRatio?: `${number}:${number}`
|
|
1352
|
-
seed?: number
|
|
1353
|
-
inputImages: Uint8Array[]
|
|
1354
|
-
maskImage?: Uint8Array
|
|
1355
|
-
json: boolean
|
|
1356
|
-
stdout: boolean
|
|
1357
|
-
}) {
|
|
1358
|
-
if (count !== 1) {
|
|
1359
|
-
console.error(pc.red('ChatGPT image generation currently supports exactly one output image.'))
|
|
1360
|
-
process.exit(1)
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
if (seed !== undefined) {
|
|
1364
|
-
console.error(pc.red('ChatGPT image generation does not support --seed.'))
|
|
1365
|
-
process.exit(1)
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
if (maskImage) {
|
|
1369
|
-
console.error(pc.red('ChatGPT image generation does not support --mask yet.'))
|
|
1370
|
-
process.exit(1)
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
const size = aspectRatio ? chatGptImageSizeFromAspectRatio(aspectRatio) : undefined
|
|
1374
|
-
if (aspectRatio && !size) {
|
|
1375
|
-
console.error(
|
|
1376
|
-
pc.red(
|
|
1377
|
-
'ChatGPT image generation only supports --aspect-ratio 1:1, 3:2, or 2:3.',
|
|
1378
|
-
),
|
|
1379
|
-
)
|
|
1380
|
-
process.exit(1)
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
const storedAuth = getChatGptAuth()
|
|
1384
|
-
if (!storedAuth?.accountId) {
|
|
1385
|
-
console.error(pc.red('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.'))
|
|
1386
|
-
process.exit(1)
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth)
|
|
1390
|
-
if (auth instanceof Error) {
|
|
1391
|
-
console.error(pc.red(auth.message))
|
|
1392
|
-
process.exit(1)
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
if (!stdout) {
|
|
1396
|
-
console.error(pc.cyan('Generating...'))
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
const content: Array<{ type: 'input_text'; text: string } | { type: 'input_image'; image_url: string }> = [
|
|
1400
|
-
{ type: 'input_text', text: prompt },
|
|
1401
|
-
...inputImages.map((image) => ({
|
|
1402
|
-
type: 'input_image' as const,
|
|
1403
|
-
image_url: imageBytesToDataUrl(image),
|
|
1404
|
-
})),
|
|
1405
|
-
]
|
|
1406
|
-
|
|
1407
|
-
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
1408
|
-
method: 'POST',
|
|
1409
|
-
headers: {
|
|
1410
|
-
Authorization: `Bearer ${auth.access}`,
|
|
1411
|
-
'ChatGPT-Account-ID': auth.accountId,
|
|
1412
|
-
'Content-Type': 'application/json',
|
|
1413
|
-
},
|
|
1414
|
-
body: JSON.stringify({
|
|
1415
|
-
model: 'gpt-5.4',
|
|
1416
|
-
instructions: 'You are Codex.',
|
|
1417
|
-
input: [
|
|
1418
|
-
{
|
|
1419
|
-
type: 'message',
|
|
1420
|
-
role: 'user',
|
|
1421
|
-
content,
|
|
1422
|
-
},
|
|
1423
|
-
],
|
|
1424
|
-
tools: [
|
|
1425
|
-
{
|
|
1426
|
-
type: 'image_generation',
|
|
1427
|
-
model,
|
|
1428
|
-
size: 'auto',
|
|
1429
|
-
quality: 'auto',
|
|
1430
|
-
output_format: 'png',
|
|
1431
|
-
output_compression: 100,
|
|
1432
|
-
moderation: 'auto',
|
|
1433
|
-
...(size ? { size } : {}),
|
|
1434
|
-
},
|
|
1435
|
-
],
|
|
1436
|
-
tool_choice: 'auto',
|
|
1437
|
-
parallel_tool_calls: true,
|
|
1438
|
-
stream: true,
|
|
1439
|
-
store: false,
|
|
1440
|
-
include: [],
|
|
1441
|
-
}),
|
|
1442
|
-
})
|
|
1443
|
-
|
|
1444
|
-
if (!response.ok || !response.body) {
|
|
1445
|
-
const body = await response.text().catch(() => '')
|
|
1446
|
-
console.error(pc.red(`ChatGPT image generation failed: ${response.status}`))
|
|
1447
|
-
if (body) console.error(pc.dim(body))
|
|
1448
|
-
process.exit(1)
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
let imageBase64: string | undefined
|
|
1452
|
-
let revisedPrompt: string | null = null
|
|
1453
|
-
|
|
1454
|
-
const processSseMessage = (message: EventSourceMessage) => {
|
|
1455
|
-
if (!message.data) return
|
|
1456
|
-
try {
|
|
1457
|
-
const event = JSON.parse(message.data) as {
|
|
1458
|
-
type?: string
|
|
1459
|
-
partial_image_b64?: string
|
|
1460
|
-
item?: { type?: string; result?: string; revised_prompt?: string | null }
|
|
1461
|
-
}
|
|
1462
|
-
if (
|
|
1463
|
-
event.type === 'response.image_generation_call.partial_image' &&
|
|
1464
|
-
event.partial_image_b64 &&
|
|
1465
|
-
!imageBase64
|
|
1466
|
-
) {
|
|
1467
|
-
imageBase64 = event.partial_image_b64
|
|
1468
|
-
}
|
|
1469
|
-
if (
|
|
1470
|
-
event.type === 'response.output_item.done' &&
|
|
1471
|
-
event.item?.type === 'image_generation_call'
|
|
1472
|
-
) {
|
|
1473
|
-
if (event.item.result) imageBase64 = event.item.result
|
|
1474
|
-
revisedPrompt = event.item.revised_prompt ?? revisedPrompt
|
|
1475
|
-
}
|
|
1476
|
-
} catch {
|
|
1477
|
-
// Ignore keepalive and partial parse noise.
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
const parser = createParser({
|
|
1482
|
-
onEvent: processSseMessage,
|
|
1483
|
-
})
|
|
1484
|
-
|
|
1485
|
-
const decoder = new TextDecoder()
|
|
1486
|
-
|
|
1487
|
-
for await (const chunk of response.body) {
|
|
1488
|
-
parser.feed(decoder.decode(chunk, { stream: true }))
|
|
1489
|
-
}
|
|
1490
|
-
parser.feed(decoder.decode())
|
|
1491
|
-
|
|
1492
|
-
if (!imageBase64) {
|
|
1493
|
-
console.error(pc.red('No images generated.'))
|
|
1494
|
-
process.exit(1)
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
const imageBytes = Buffer.from(imageBase64, 'base64')
|
|
1498
|
-
const mediaType = 'image/png'
|
|
1499
|
-
|
|
1500
|
-
if (stdout) {
|
|
1501
|
-
process.stdout.write(imageBytes)
|
|
1502
|
-
return
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
const savedFiles: string[] = []
|
|
1506
|
-
const filePath = ensureExtension(outputPath, extensionFromMediaType(mediaType))
|
|
1507
|
-
fs.writeFileSync(filePath, imageBytes)
|
|
1508
|
-
console.error(pc.green(`Saved: ${filePath}`))
|
|
1509
|
-
savedFiles.push(filePath)
|
|
1510
|
-
|
|
1511
|
-
if (revisedPrompt && !json) {
|
|
1512
|
-
console.error(pc.dim(`Revised prompt: ${revisedPrompt}`))
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
if (json) {
|
|
1516
|
-
const output = {
|
|
1517
|
-
model,
|
|
1518
|
-
files: savedFiles,
|
|
1519
|
-
count: 1,
|
|
1520
|
-
revisedPrompt,
|
|
1521
|
-
}
|
|
1522
|
-
console.log(JSON.stringify(output, null, 2))
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
// Generate using experimental_generateVideo (video models)
|
|
1527
|
-
async function generateWithVideoModel({
|
|
1528
|
-
prompt,
|
|
1529
|
-
model,
|
|
1530
|
-
outputPath,
|
|
1531
|
-
count,
|
|
1532
|
-
aspectRatio,
|
|
1533
|
-
resolution,
|
|
1534
|
-
duration,
|
|
1535
|
-
fps,
|
|
1536
|
-
seed,
|
|
1537
|
-
inputImage,
|
|
1538
|
-
mode,
|
|
1539
|
-
videoUrl,
|
|
1540
|
-
referenceImages,
|
|
1541
|
-
negativePrompt,
|
|
1542
|
-
json,
|
|
1543
|
-
stdout,
|
|
1544
|
-
}: {
|
|
1545
|
-
prompt: string
|
|
1546
|
-
model: string
|
|
1547
|
-
outputPath: string
|
|
1548
|
-
count: number
|
|
1549
|
-
aspectRatio?: string
|
|
1550
|
-
resolution?: string
|
|
1551
|
-
duration?: number
|
|
1552
|
-
fps?: number
|
|
1553
|
-
seed?: number
|
|
1554
|
-
inputImage?: Uint8Array
|
|
1555
|
-
mode?: string
|
|
1556
|
-
videoUrl?: string
|
|
1557
|
-
referenceImages?: string[]
|
|
1558
|
-
negativePrompt?: string
|
|
1559
|
-
json: boolean
|
|
1560
|
-
stdout: boolean
|
|
1561
|
-
}) {
|
|
1562
|
-
const videoModel = await createVideoModel(model)
|
|
1563
|
-
const config = getModelConfig(model)
|
|
1564
|
-
|
|
1565
|
-
// Validate that the model supports video-url input before building options.
|
|
1566
|
-
// Only models with a 'video-url' providerOption (currently xAI) accept edit/extend mode.
|
|
1567
|
-
if (videoUrl && !config.providerOptions?.some((opt) => opt.flag === 'video-url')) {
|
|
1568
|
-
console.error(pc.red(`Model ${model} does not support video URL input for editing/extension`))
|
|
1569
|
-
process.exit(1)
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
// Build type-safe provider options for video generation.
|
|
1573
|
-
const providerOptions = buildVideoProviderOptions(config.provider, {
|
|
1574
|
-
resolution,
|
|
1575
|
-
mode,
|
|
1576
|
-
videoUrl,
|
|
1577
|
-
referenceImages,
|
|
1578
|
-
negativePrompt,
|
|
1579
|
-
model,
|
|
1580
|
-
})
|
|
1581
|
-
|
|
1582
|
-
if (!stdout) {
|
|
1583
|
-
console.error(pc.cyan('Generating...'))
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
const result = await runProviderCall('Video generation', () => aiGenerateVideo({
|
|
1587
|
-
model: videoModel,
|
|
1588
|
-
prompt: inputImage
|
|
1589
|
-
? { image: inputImage, text: prompt }
|
|
1590
|
-
: prompt,
|
|
1591
|
-
n: count,
|
|
1592
|
-
...(aspectRatio ? { aspectRatio: aspectRatio as `${number}:${number}` } : {}),
|
|
1593
|
-
...(resolution ? { resolution: resolution as `${number}x${number}` } : {}),
|
|
1594
|
-
...(duration != null ? { duration } : {}),
|
|
1595
|
-
...(fps != null ? { fps } : {}),
|
|
1596
|
-
...(seed != null ? { seed } : {}),
|
|
1597
|
-
...(providerOptions ? { providerOptions } : {}),
|
|
1598
|
-
}))
|
|
1599
|
-
|
|
1600
|
-
const files = result.videos.map((v: { uint8Array: Uint8Array; mediaType: string }) => ({
|
|
1601
|
-
uint8Array: v.uint8Array,
|
|
1602
|
-
mediaType: v.mediaType,
|
|
1603
|
-
}))
|
|
1604
|
-
|
|
1605
|
-
if (stdout) {
|
|
1606
|
-
writeFirstToStdout(files)
|
|
1607
|
-
return
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
const savedFiles = saveGeneratedFiles(files, outputPath)
|
|
1611
|
-
const cost = calculateCost(config.cost, {
|
|
1612
|
-
videosGenerated: result.videos.length,
|
|
1613
|
-
durationSeconds: duration,
|
|
1614
|
-
resolution,
|
|
1615
|
-
}, result.videos.length)
|
|
1616
|
-
printCost(cost)
|
|
1617
|
-
|
|
1618
|
-
if (json) {
|
|
1619
|
-
console.log(JSON.stringify({
|
|
1620
|
-
model,
|
|
1621
|
-
files: savedFiles,
|
|
1622
|
-
count: result.videos.length,
|
|
1623
|
-
cost,
|
|
1624
|
-
warnings: result.warnings,
|
|
1625
|
-
responses: result.responses,
|
|
1626
|
-
}, null, 2))
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
1684
|
// ─── shared output helpers ───────────────────────────────────────────────────
|
|
1631
|
-
// Common logic for saving generated files, calculating cost, and printing JSON
|
|
1632
|
-
// metadata. Used by all generation paths to avoid repeating boilerplate.
|
|
1633
|
-
|
|
1634
|
-
type GeneratedFile = {
|
|
1635
|
-
uint8Array: Uint8Array
|
|
1636
|
-
mediaType: string
|
|
1637
|
-
}
|
|
1638
1685
|
|
|
1639
|
-
/**
|
|
1640
|
-
* Save generated files to disk. Returns the list of saved file paths.
|
|
1641
|
-
* Handles single vs multi-file naming with index suffixes.
|
|
1642
|
-
*/
|
|
1643
1686
|
function saveGeneratedFiles(
|
|
1644
1687
|
files: GeneratedFile[],
|
|
1645
1688
|
outputPath: string,
|
|
@@ -1660,18 +1703,12 @@ function saveGeneratedFiles(
|
|
|
1660
1703
|
return savedFiles
|
|
1661
1704
|
}
|
|
1662
1705
|
|
|
1663
|
-
/**
|
|
1664
|
-
* Print estimated cost to stderr if calculable.
|
|
1665
|
-
*/
|
|
1666
1706
|
function printCost(cost: number | null): void {
|
|
1667
1707
|
if (cost != null) {
|
|
1668
1708
|
console.error(pc.dim(`Cost: ${formatCost(cost)}`))
|
|
1669
1709
|
}
|
|
1670
1710
|
}
|
|
1671
1711
|
|
|
1672
|
-
/**
|
|
1673
|
-
* Write the first file's raw bytes to stdout (for piping to other tools).
|
|
1674
|
-
*/
|
|
1675
1712
|
function writeFirstToStdout(files: GeneratedFile[]): void {
|
|
1676
1713
|
const file = files[0]
|
|
1677
1714
|
if (file) {
|
|
@@ -1679,54 +1716,7 @@ function writeFirstToStdout(files: GeneratedFile[]): void {
|
|
|
1679
1716
|
}
|
|
1680
1717
|
}
|
|
1681
1718
|
|
|
1682
|
-
// ───
|
|
1683
|
-
|
|
1684
|
-
function calculateCost(
|
|
1685
|
-
cost: {
|
|
1686
|
-
type: 'per-image'
|
|
1687
|
-
perImage: number
|
|
1688
|
-
} | {
|
|
1689
|
-
type: 'per-token'
|
|
1690
|
-
inputPerM: number
|
|
1691
|
-
outputPerM: number
|
|
1692
|
-
} | {
|
|
1693
|
-
type: 'per-video-second'
|
|
1694
|
-
defaultDurationSec: number
|
|
1695
|
-
tiers: Array<{ resolution?: string; costPerSecond: number }>
|
|
1696
|
-
} | {
|
|
1697
|
-
type: 'unknown'
|
|
1698
|
-
},
|
|
1699
|
-
usage: {
|
|
1700
|
-
inputTokens?: number
|
|
1701
|
-
outputTokens?: number
|
|
1702
|
-
imagesGenerated?: number
|
|
1703
|
-
videosGenerated?: number
|
|
1704
|
-
durationSeconds?: number
|
|
1705
|
-
resolution?: string
|
|
1706
|
-
},
|
|
1707
|
-
count: number = 1,
|
|
1708
|
-
): number | null {
|
|
1709
|
-
if (cost.type === 'per-image') {
|
|
1710
|
-
return cost.perImage * count
|
|
1711
|
-
}
|
|
1712
|
-
if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
|
|
1713
|
-
return (
|
|
1714
|
-
(usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000
|
|
1715
|
-
)
|
|
1716
|
-
}
|
|
1717
|
-
if (cost.type === 'per-video-second') {
|
|
1718
|
-
const durationSec = usage.durationSeconds ?? cost.defaultDurationSec
|
|
1719
|
-
const resolution = normalizeResolutionKey(usage.resolution)
|
|
1720
|
-
const tier =
|
|
1721
|
-
cost.tiers.find((t) => normalizeResolutionKey(t.resolution) === resolution) ??
|
|
1722
|
-
cost.tiers[0]
|
|
1723
|
-
if (!tier) {
|
|
1724
|
-
return null
|
|
1725
|
-
}
|
|
1726
|
-
return tier.costPerSecond * durationSec * count
|
|
1727
|
-
}
|
|
1728
|
-
return null
|
|
1729
|
-
}
|
|
1719
|
+
// ─── display helpers ─────────────────────────────────────────────────────────
|
|
1730
1720
|
|
|
1731
1721
|
function formatCatalogCost(
|
|
1732
1722
|
cost: {
|
|
@@ -1740,6 +1730,12 @@ function formatCatalogCost(
|
|
|
1740
1730
|
type: 'per-video-second'
|
|
1741
1731
|
defaultDurationSec: number
|
|
1742
1732
|
tiers: Array<{ resolution?: string; mode?: string; audio?: boolean; costPerSecond: number }>
|
|
1733
|
+
} | {
|
|
1734
|
+
type: 'per-character'
|
|
1735
|
+
perMillionChars: number
|
|
1736
|
+
} | {
|
|
1737
|
+
type: 'per-second'
|
|
1738
|
+
perSecond: number
|
|
1743
1739
|
} | {
|
|
1744
1740
|
type: 'unknown'
|
|
1745
1741
|
},
|
|
@@ -1750,6 +1746,12 @@ function formatCatalogCost(
|
|
|
1750
1746
|
if (cost.type === 'per-token') {
|
|
1751
1747
|
return `$${cost.inputPerM}/M input, $${cost.outputPerM}/M output`
|
|
1752
1748
|
}
|
|
1749
|
+
if (cost.type === 'per-character') {
|
|
1750
|
+
return `$${cost.perMillionChars}/M chars`
|
|
1751
|
+
}
|
|
1752
|
+
if (cost.type === 'per-second') {
|
|
1753
|
+
return `${formatCost(cost.perSecond * 60)}/min`
|
|
1754
|
+
}
|
|
1753
1755
|
if (cost.type === 'per-video-second') {
|
|
1754
1756
|
const tierText = cost.tiers
|
|
1755
1757
|
.map((t) => {
|
|
@@ -1769,6 +1771,9 @@ function formatCatalogCost(
|
|
|
1769
1771
|
}
|
|
1770
1772
|
|
|
1771
1773
|
function formatCost(dollars: number): string {
|
|
1774
|
+
if (dollars < 0.0001) {
|
|
1775
|
+
return `$${dollars.toFixed(6)}`
|
|
1776
|
+
}
|
|
1772
1777
|
if (dollars < 0.01) {
|
|
1773
1778
|
return `$${dollars.toFixed(4)}`
|
|
1774
1779
|
}
|
|
@@ -1784,71 +1789,18 @@ function extensionFromMediaType(mediaType: string): string {
|
|
|
1784
1789
|
'video/mp4': '.mp4',
|
|
1785
1790
|
'video/webm': '.webm',
|
|
1786
1791
|
'video/quicktime': '.mov',
|
|
1792
|
+
'audio/mpeg': '.mp3',
|
|
1793
|
+
'audio/wav': '.wav',
|
|
1794
|
+
'audio/pcm': '.pcm',
|
|
1795
|
+
'audio/opus': '.opus',
|
|
1796
|
+
'audio/aac': '.aac',
|
|
1797
|
+
'audio/flac': '.flac',
|
|
1798
|
+
'audio/ogg': '.ogg',
|
|
1787
1799
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
if (image.length >= 8 && image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) {
|
|
1793
|
-
return 'image/png'
|
|
1794
|
-
}
|
|
1795
|
-
if (image.length >= 3 && image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) {
|
|
1796
|
-
return 'image/jpeg'
|
|
1797
|
-
}
|
|
1798
|
-
if (
|
|
1799
|
-
image.length >= 12 &&
|
|
1800
|
-
image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
|
|
1801
|
-
image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50
|
|
1802
|
-
) {
|
|
1803
|
-
return 'image/webp'
|
|
1804
|
-
}
|
|
1805
|
-
if (
|
|
1806
|
-
image.length >= 6 &&
|
|
1807
|
-
image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38
|
|
1808
|
-
) {
|
|
1809
|
-
return 'image/gif'
|
|
1810
|
-
}
|
|
1811
|
-
return 'image/png'
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
function imageBytesToDataUrl(image: Uint8Array): string {
|
|
1815
|
-
const mediaType = inferImageMediaType(image)
|
|
1816
|
-
return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
function chatGptImageSizeFromAspectRatio(
|
|
1820
|
-
aspectRatio: `${number}:${number}`,
|
|
1821
|
-
): '1024x1024' | '1536x1024' | '1024x1536' | undefined {
|
|
1822
|
-
switch (aspectRatio) {
|
|
1823
|
-
case '1:1':
|
|
1824
|
-
return '1024x1024'
|
|
1825
|
-
case '3:2':
|
|
1826
|
-
return '1536x1024'
|
|
1827
|
-
case '2:3':
|
|
1828
|
-
return '1024x1536'
|
|
1829
|
-
default:
|
|
1830
|
-
return undefined
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
function normalizeResolutionKey(input?: string): string | undefined {
|
|
1835
|
-
if (!input) return undefined
|
|
1836
|
-
const normalized = input.trim().toLowerCase()
|
|
1837
|
-
if (normalized === '1920x1080') return '1080p'
|
|
1838
|
-
if (normalized === '1280x720') return '720p'
|
|
1839
|
-
if (normalized === '854x480' || normalized === '848x480') return '480p'
|
|
1840
|
-
return normalized
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function isAspectRatio(input: string): input is `${number}:${number}` {
|
|
1844
|
-
return /^\d+:\d+$/.test(input)
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
function parseAspectRatioOption(input?: string): `${number}:${number}` | undefined {
|
|
1848
|
-
if (!input || !isAspectRatio(input)) {
|
|
1849
|
-
return undefined
|
|
1850
|
-
}
|
|
1851
|
-
return input
|
|
1800
|
+
if (map[mediaType]) return map[mediaType]!
|
|
1801
|
+
if (mediaType.startsWith('audio/')) return '.mp3'
|
|
1802
|
+
if (mediaType.startsWith('video/')) return '.mp4'
|
|
1803
|
+
return '.png'
|
|
1852
1804
|
}
|
|
1853
1805
|
|
|
1854
1806
|
function ensureExtension(filePath: string, ext: string): string {
|