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
|
@@ -10,19 +10,26 @@
|
|
|
10
10
|
import { goke } from 'goke';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import dedent from 'string-dedent';
|
|
13
|
-
import { APICallError,
|
|
13
|
+
import { APICallError, NoImageGeneratedError, NoVideoGeneratedError, RetryError, } from 'ai';
|
|
14
14
|
import { select, isCancel, cancel } from '@clack/prompts';
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import pc from 'picocolors';
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, } from './models.js';
|
|
18
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
19
|
+
import { injectCredentialsToEnv, PROVIDERS, getKeyStatus, } from './credentials.js';
|
|
20
|
+
import { DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, } from './models.js';
|
|
22
21
|
import { CATALOG, VIDEO_CATALOG, describeProviderValues, } from './model-catalog.js';
|
|
23
22
|
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
24
23
|
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
25
|
-
import {
|
|
24
|
+
import { generateImageUncached as generateImage, generateVideoUncached as generateVideo, calculateCost, ValidationError, } from './generate.js';
|
|
25
|
+
import { generateSpeechUncached as generateSpeech, } from './speech-generate.js';
|
|
26
|
+
import { transcribeAudioUncached as transcribeAudio, segmentsToWordTimestamps, } from './transcription-generate.js';
|
|
27
|
+
import { SPEECH_CATALOG, } from './speech-catalog.js';
|
|
28
|
+
import { TRANSCRIPTION_CATALOG, } from './transcription-catalog.js';
|
|
29
|
+
import { DEFAULT_SPEECH_MODEL, } from './speech-models.js';
|
|
30
|
+
import { DEFAULT_TRANSCRIPTION_MODEL, } from './transcription-models.js';
|
|
31
|
+
import { separateAudioUncached as separateAudio, downloadStem, } from './separation-generate.js';
|
|
32
|
+
import { cloneVoiceUncached as cloneVoice, } from './clone-generate.js';
|
|
26
33
|
const cli = goke('egaki');
|
|
27
34
|
process.title = 'egaki';
|
|
28
35
|
process.on('uncaughtException', (err) => {
|
|
@@ -191,72 +198,55 @@ cli
|
|
|
191
198
|
.example('# Pipe to another tool')
|
|
192
199
|
.example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
|
|
193
200
|
.action(async (prompt, options) => {
|
|
194
|
-
// Inject stored API keys as env vars before calling the AI SDK.
|
|
195
|
-
injectCredentialsToEnv();
|
|
196
201
|
const model = options.model ?? await resolveImageModel();
|
|
197
202
|
const outputPath = options.output ?? 'egaki-output.png';
|
|
198
|
-
const count = options.count ?? 1;
|
|
199
|
-
const config = getModelConfig(model);
|
|
200
|
-
const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio);
|
|
201
203
|
if (!options.stdout) {
|
|
202
204
|
console.error(pc.dim(`Model: ${model}`));
|
|
203
205
|
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
206
|
+
console.error(pc.cyan('Generating...'));
|
|
204
207
|
}
|
|
205
208
|
const inputImages = await readInputImages(options.input);
|
|
206
209
|
const maskImage = options.mask
|
|
207
210
|
? await readInputSource(options.mask)
|
|
208
211
|
: undefined;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
stdout: options.stdout || false,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
else if (config.strategy === 'image') {
|
|
231
|
-
await generateWithImageModel({
|
|
232
|
-
prompt,
|
|
233
|
-
model,
|
|
234
|
-
outputPath,
|
|
235
|
-
count,
|
|
236
|
-
aspectRatio: parsedAspectRatio,
|
|
237
|
-
seed: options.seed,
|
|
238
|
-
inputImages,
|
|
239
|
-
maskImage,
|
|
240
|
-
allowPeople: options.allowPeople || false,
|
|
241
|
-
quality: options.quality,
|
|
242
|
-
resolution: options.resolution,
|
|
243
|
-
outputFormat: options.outputFormat,
|
|
244
|
-
negativePrompt: options.negativePrompt,
|
|
245
|
-
json: options.json || false,
|
|
246
|
-
stdout: options.stdout || false,
|
|
247
|
-
});
|
|
212
|
+
const result = await runProviderCall('Image generation', () => generateImage({
|
|
213
|
+
prompt,
|
|
214
|
+
model,
|
|
215
|
+
count: options.count ?? 1,
|
|
216
|
+
aspectRatio: options.aspectRatio,
|
|
217
|
+
seed: options.seed,
|
|
218
|
+
inputImages,
|
|
219
|
+
maskImage,
|
|
220
|
+
allowPeople: options.allowPeople || false,
|
|
221
|
+
quality: options.quality,
|
|
222
|
+
resolution: options.resolution,
|
|
223
|
+
outputFormat: options.outputFormat,
|
|
224
|
+
negativePrompt: options.negativePrompt,
|
|
225
|
+
imageSize: options.imageSize,
|
|
226
|
+
}));
|
|
227
|
+
if (options.stdout) {
|
|
228
|
+
writeFirstToStdout(result.images);
|
|
229
|
+
return;
|
|
248
230
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
231
|
+
const savedFiles = saveGeneratedFiles(result.images, outputPath);
|
|
232
|
+
printCost(result.cost);
|
|
233
|
+
if (result.revisedPrompt && !options.json) {
|
|
234
|
+
console.error(pc.dim(`Revised prompt: ${result.revisedPrompt}`));
|
|
235
|
+
}
|
|
236
|
+
if (result.text && !options.json) {
|
|
237
|
+
console.error(pc.dim(result.text));
|
|
238
|
+
}
|
|
239
|
+
if (options.json) {
|
|
240
|
+
console.log(JSON.stringify({
|
|
252
241
|
model,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
242
|
+
files: savedFiles,
|
|
243
|
+
count: result.images.length,
|
|
244
|
+
cost: result.cost,
|
|
245
|
+
usage: result.usage,
|
|
246
|
+
warnings: result.warnings,
|
|
247
|
+
...(result.text ? { text: result.text } : {}),
|
|
248
|
+
...(result.revisedPrompt !== undefined ? { revisedPrompt: result.revisedPrompt } : {}),
|
|
249
|
+
}, null, 2));
|
|
260
250
|
}
|
|
261
251
|
});
|
|
262
252
|
// ─── video command ───────────────────────────────────────────────────────────
|
|
@@ -289,7 +279,7 @@ cli
|
|
|
289
279
|
.describe(`Video operation mode. ${describeProviderValues('mode', [VIDEO_CATALOG])}`))
|
|
290
280
|
.option('--reference-images [path]', z
|
|
291
281
|
.array(z.string())
|
|
292
|
-
.describe('Reference images for R2V generation, repeatable
|
|
282
|
+
.describe('Reference images for R2V generation, repeatable. xAI: 1-7 images. Seedance: 1-9 images (use @Image1 etc. in prompt)'))
|
|
293
283
|
.option('--negative-prompt [text]', z
|
|
294
284
|
.string()
|
|
295
285
|
.describe('Describe what to avoid in the generated video (Fal models)'))
|
|
@@ -309,50 +299,41 @@ cli
|
|
|
309
299
|
injectCredentialsToEnv();
|
|
310
300
|
const model = options.model ?? await resolveVideoModel();
|
|
311
301
|
const outputPath = options.output ?? 'egaki-output.mp4';
|
|
312
|
-
const count = options.count ?? 1;
|
|
313
302
|
const config = getModelConfig(model);
|
|
314
|
-
if (config
|
|
315
|
-
console.error(pc.red(
|
|
303
|
+
if (config instanceof Error) {
|
|
304
|
+
console.error(pc.red(config.message));
|
|
316
305
|
process.exit(1);
|
|
317
306
|
}
|
|
318
307
|
if (!options.stdout) {
|
|
319
308
|
console.error(pc.dim(`Model: ${model}`));
|
|
320
309
|
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
321
|
-
console.error(pc.
|
|
310
|
+
console.error(pc.cyan('Generating...'));
|
|
322
311
|
}
|
|
323
312
|
// Determine how --input is used based on --mode:
|
|
324
313
|
// - edit-video / extend-video: input is a source video, passed as videoUrl (needs URL)
|
|
325
314
|
// - everything else (i2v, t2v): input is a reference image, passed as bytes
|
|
326
315
|
const isVideoInputMode = options.mode === 'edit-video' || options.mode === 'extend-video';
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
if (options.mode === 'reference-to-video' && (!options.referenceImages || options.referenceImages.length === 0)) {
|
|
333
|
-
console.error(pc.red('--reference-images is required with --mode reference-to-video. Pass 1-7 image files or URLs.'));
|
|
334
|
-
process.exit(1);
|
|
316
|
+
// Auto-set --mode reference-to-video for providers that require it (xAI) when
|
|
317
|
+
// --reference-images is provided without an explicit --mode.
|
|
318
|
+
if (options.referenceImages?.length && !options.mode && config.provider === 'xai') {
|
|
319
|
+
options.mode = 'reference-to-video';
|
|
335
320
|
}
|
|
336
321
|
let inputImage;
|
|
337
322
|
let videoUrl;
|
|
338
323
|
if (options.input && isVideoInputMode) {
|
|
339
|
-
// Upload local file or use URL directly for video editing/extension
|
|
340
324
|
videoUrl = await resolveToUrl(options.input);
|
|
341
325
|
}
|
|
342
326
|
else if (options.input) {
|
|
343
|
-
// Read as bytes for image-to-video
|
|
344
327
|
inputImage = await readInputSource(options.input);
|
|
345
328
|
}
|
|
346
|
-
// Resolve --reference-images: upload local files to get URLs
|
|
347
329
|
let resolvedReferenceImages = options.referenceImages;
|
|
348
330
|
if (resolvedReferenceImages) {
|
|
349
331
|
resolvedReferenceImages = await Promise.all(resolvedReferenceImages.map((ref) => resolveToUrl(ref)));
|
|
350
332
|
}
|
|
351
|
-
await
|
|
333
|
+
const result = await runProviderCall('Video generation', () => generateVideo({
|
|
352
334
|
prompt,
|
|
353
335
|
model,
|
|
354
|
-
|
|
355
|
-
count,
|
|
336
|
+
count: options.count ?? 1,
|
|
356
337
|
aspectRatio: options.aspectRatio,
|
|
357
338
|
resolution: options.resolution,
|
|
358
339
|
duration: options.duration,
|
|
@@ -363,9 +344,425 @@ cli
|
|
|
363
344
|
videoUrl,
|
|
364
345
|
referenceImages: resolvedReferenceImages,
|
|
365
346
|
negativePrompt: options.negativePrompt,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
347
|
+
}));
|
|
348
|
+
if (options.stdout) {
|
|
349
|
+
writeFirstToStdout(result.videos);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const savedFiles = saveGeneratedFiles(result.videos, outputPath);
|
|
353
|
+
printCost(result.cost);
|
|
354
|
+
if (options.json) {
|
|
355
|
+
console.log(JSON.stringify({
|
|
356
|
+
model,
|
|
357
|
+
files: savedFiles,
|
|
358
|
+
count: result.videos.length,
|
|
359
|
+
cost: result.cost,
|
|
360
|
+
warnings: result.warnings,
|
|
361
|
+
responses: result.responses,
|
|
362
|
+
}, null, 2));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// ─── speech command ──────────────────────────────────────────────────────────
|
|
366
|
+
cli
|
|
367
|
+
.command('speech [text]', dedent `
|
|
368
|
+
Generate speech audio from text using AI text-to-speech models.
|
|
369
|
+
Supports OpenAI (tts-1, tts-1-hd, gpt-4o-mini-tts), ElevenLabs
|
|
370
|
+
(eleven_v3, eleven_multilingual_v2, eleven_flash_v2_5), and Cartesia
|
|
371
|
+
(sonic-3.5, sonic-3). Uses your provider API key directly (egaki
|
|
372
|
+
subscription does not cover speech yet).
|
|
373
|
+
`)
|
|
374
|
+
.option('-m, --model [model]', z.string().describe('Speech model ID. If omitted, shows an interactive picker (or uses default in non-TTY mode)'))
|
|
375
|
+
.option('-o, --output [path]', z
|
|
376
|
+
.string()
|
|
377
|
+
.default('egaki-output')
|
|
378
|
+
.describe('Output file path (extension added from audio format if omitted)'))
|
|
379
|
+
.option('--voice [voice]', z
|
|
380
|
+
.string()
|
|
381
|
+
.describe('Voice ID or name (provider-specific). OpenAI: alloy, echo, nova, etc. ElevenLabs/Cartesia: voice ID from their library'))
|
|
382
|
+
.option('--output-format [format]', z
|
|
383
|
+
.string()
|
|
384
|
+
.describe('Audio output format: mp3, wav, pcm, opus, aac, flac (provider support varies)'))
|
|
385
|
+
.option('--speed [speed]', z
|
|
386
|
+
.number()
|
|
387
|
+
.describe('Playback speed multiplier (OpenAI: 0.25-4.0, Cartesia: 0.6-1.5)'))
|
|
388
|
+
.option('--instructions [text]', z
|
|
389
|
+
.string()
|
|
390
|
+
.describe('Style instructions for speech (only gpt-4o-mini-tts and ElevenLabs). E.g. "Speak in a calm, soothing tone"'))
|
|
391
|
+
.option('--language [lang]', z
|
|
392
|
+
.string()
|
|
393
|
+
.describe('ISO 639-1 language code (e.g. en, es, fr). ElevenLabs and Cartesia'))
|
|
394
|
+
.option('--stdin', 'Read text from stdin instead of the positional argument')
|
|
395
|
+
.option('--json', 'Output result metadata as JSON to stdout (model, file path, cost)')
|
|
396
|
+
.option('--stdout', 'Write raw audio bytes to stdout instead of saving to a file. Useful for piping')
|
|
397
|
+
.example('# Generate speech with default model')
|
|
398
|
+
.example('egaki speech "Hello, welcome to egaki!"')
|
|
399
|
+
.example('# Use a specific model and voice')
|
|
400
|
+
.example('egaki speech "Good morning" -m tts-1-hd --voice nova')
|
|
401
|
+
.example('# Use GPT-4o Mini TTS with style instructions')
|
|
402
|
+
.example('egaki speech "Breaking news today" -m gpt-4o-mini-tts --instructions "Speak like a news anchor"')
|
|
403
|
+
.example('# ElevenLabs with a voice ID')
|
|
404
|
+
.example('egaki speech "Hello world" -m eleven_v3 --voice 21m00Tcm4TlvDq8ikWAM')
|
|
405
|
+
.example('# Read text from stdin')
|
|
406
|
+
.example('cat script.txt | egaki speech --stdin -o narration.mp3')
|
|
407
|
+
.example('# Cartesia Sonic 3.5 with a voice ID')
|
|
408
|
+
.example('egaki speech "Hello world" -m sonic-3.5 --voice f786b574-daa5-4673-aa0c-cbe3e8534c02')
|
|
409
|
+
.example('# Pipe audio to another tool')
|
|
410
|
+
.example('egaki speech "test" --stdout | ffplay -nodisp -autoexit -')
|
|
411
|
+
.action(async (text = '', options) => {
|
|
412
|
+
let inputText = text;
|
|
413
|
+
if (options.stdin) {
|
|
414
|
+
inputText = await readTextFromStdin();
|
|
415
|
+
}
|
|
416
|
+
if (!inputText || inputText.trim().length === 0) {
|
|
417
|
+
console.error(pc.red('No text provided. Pass text as an argument or use --stdin.'));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
const model = options.model ?? await resolveSpeechModel();
|
|
421
|
+
const outputPath = options.output ?? 'egaki-output';
|
|
422
|
+
if (!options.stdout) {
|
|
423
|
+
console.error(pc.dim(`Model: ${model}`));
|
|
424
|
+
console.error(pc.dim(`Text: ${inputText.length > 80 ? inputText.slice(0, 80) + '...' : inputText}`));
|
|
425
|
+
console.error(pc.cyan('Generating speech...'));
|
|
426
|
+
}
|
|
427
|
+
const result = await runProviderCall('Speech generation', () => generateSpeech({
|
|
428
|
+
text: inputText,
|
|
429
|
+
model,
|
|
430
|
+
voice: options.voice,
|
|
431
|
+
outputFormat: options.outputFormat,
|
|
432
|
+
instructions: options.instructions,
|
|
433
|
+
speed: options.speed,
|
|
434
|
+
language: options.language,
|
|
435
|
+
}));
|
|
436
|
+
if (options.stdout) {
|
|
437
|
+
process.stdout.write(Buffer.from(result.audio.uint8Array));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const ext = extensionFromMediaType(result.audio.mediaType);
|
|
441
|
+
const finalPath = ensureExtension(outputPath, ext);
|
|
442
|
+
fs.writeFileSync(finalPath, result.audio.uint8Array);
|
|
443
|
+
console.error(pc.green(`Saved: ${finalPath}`));
|
|
444
|
+
printCost(result.cost);
|
|
445
|
+
if (options.json) {
|
|
446
|
+
console.log(JSON.stringify({
|
|
447
|
+
model,
|
|
448
|
+
file: finalPath,
|
|
449
|
+
cost: result.cost,
|
|
450
|
+
textLength: inputText.length,
|
|
451
|
+
timestamps: result.timestamps ? result.timestamps.length : 0,
|
|
452
|
+
}, null, 2));
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
// ─── transcribe command ──────────────────────────────────────────────────────
|
|
456
|
+
cli
|
|
457
|
+
.command('transcribe [audio]', dedent `
|
|
458
|
+
Transcribe audio to text using AI speech-to-text models.
|
|
459
|
+
Supports OpenAI (whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe),
|
|
460
|
+
ElevenLabs (scribe_v1), Deepgram (nova-3), Groq (whisper-large-v3,
|
|
461
|
+
whisper-large-v3-turbo, distil-whisper-large-v3-en), and Cartesia
|
|
462
|
+
(ink-whisper). Outputs JSON with text, timestamps, language, and duration.
|
|
463
|
+
`)
|
|
464
|
+
.option('-m, --model [model]', z.string().describe('Transcription model ID. If omitted, shows an interactive picker (or uses default in non-TTY mode)'))
|
|
465
|
+
.option('-o, --output [path]', z
|
|
466
|
+
.string()
|
|
467
|
+
.describe('Output file path for the JSON result'))
|
|
468
|
+
.option('--language [lang]', z
|
|
469
|
+
.string()
|
|
470
|
+
.describe('ISO 639-1 language hint (e.g. en, es, fr). Helps accuracy for known languages'))
|
|
471
|
+
.option('--stdin', 'Read audio from stdin instead of a file path')
|
|
472
|
+
.option('--stdout', 'Write only the plain text transcript to stdout (no JSON metadata)')
|
|
473
|
+
.example('# Transcribe an audio file with default model')
|
|
474
|
+
.example('egaki transcribe recording.mp3')
|
|
475
|
+
.example('# Use a specific model')
|
|
476
|
+
.example('egaki transcribe interview.wav -m gpt-4o-transcribe')
|
|
477
|
+
.example('# Groq for fast, cheap transcription')
|
|
478
|
+
.example('egaki transcribe podcast.mp3 -m whisper-large-v3')
|
|
479
|
+
.example('# Get plain text only')
|
|
480
|
+
.example('egaki transcribe recording.mp3 --stdout')
|
|
481
|
+
.example('# Save JSON result to file')
|
|
482
|
+
.example('egaki transcribe recording.mp3 -o transcript.json')
|
|
483
|
+
.example('# Pipe audio from another command')
|
|
484
|
+
.example('ffmpeg -i video.mp4 -f mp3 - | egaki transcribe --stdin -m whisper-1')
|
|
485
|
+
.action(async (audioPath = '', options) => {
|
|
486
|
+
if (!options.stdin && !audioPath) {
|
|
487
|
+
console.error(pc.red('No audio provided. Pass an audio file path or use --stdin.'));
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
let audioData;
|
|
491
|
+
if (options.stdin) {
|
|
492
|
+
const chunks = [];
|
|
493
|
+
for await (const chunk of process.stdin) {
|
|
494
|
+
chunks.push(chunk);
|
|
495
|
+
}
|
|
496
|
+
audioData = new Uint8Array(Buffer.concat(chunks));
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const resolved = path.resolve(audioPath);
|
|
500
|
+
if (!fs.existsSync(resolved)) {
|
|
501
|
+
console.error(pc.red(`File not found: ${resolved}`));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
audioData = new Uint8Array(fs.readFileSync(resolved));
|
|
505
|
+
}
|
|
506
|
+
if (audioData.length === 0) {
|
|
507
|
+
console.error(pc.red('No audio data provided. Pass an audio file path or use --stdin.'));
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
const model = options.model ?? await resolveTranscriptionModel();
|
|
511
|
+
if (!options.stdout) {
|
|
512
|
+
console.error(pc.dim(`Model: ${model}`));
|
|
513
|
+
console.error(pc.dim(`Audio: ${options.stdin ? 'stdin' : audioPath} (${(audioData.length / 1024).toFixed(1)} KB)`));
|
|
514
|
+
console.error(pc.cyan('Transcribing...'));
|
|
515
|
+
}
|
|
516
|
+
const result = await runProviderCall('Transcription', () => transcribeAudio({
|
|
517
|
+
audio: audioData,
|
|
518
|
+
model,
|
|
519
|
+
language: options.language,
|
|
520
|
+
}));
|
|
521
|
+
if (options.stdout) {
|
|
522
|
+
process.stdout.write(result.text);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const output = {
|
|
526
|
+
text: result.text,
|
|
527
|
+
segments: result.segments,
|
|
528
|
+
wordTimestamps: segmentsToWordTimestamps(result.segments),
|
|
529
|
+
language: result.language,
|
|
530
|
+
durationInSeconds: result.durationInSeconds,
|
|
531
|
+
model: result.model,
|
|
532
|
+
cost: result.cost,
|
|
533
|
+
};
|
|
534
|
+
if (options.output) {
|
|
535
|
+
const outputPath = path.resolve(options.output);
|
|
536
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
|
|
537
|
+
console.error(pc.green(`Saved: ${outputPath}`));
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
console.log(JSON.stringify(output, null, 2));
|
|
541
|
+
}
|
|
542
|
+
printCost(result.cost);
|
|
543
|
+
});
|
|
544
|
+
// ─── demucs command ──────────────────────────────────────────────────────────
|
|
545
|
+
cli
|
|
546
|
+
.command('demucs [audio]', dedent `
|
|
547
|
+
Separate audio into stems (vocals, background, drums, bass, etc.)
|
|
548
|
+
using fal.ai's Demucs model. Requires a fal.ai API key.
|
|
549
|
+
By default extracts vocals and background (other) stems.
|
|
550
|
+
`)
|
|
551
|
+
.option('--stems [list]', z
|
|
552
|
+
.string()
|
|
553
|
+
.describe('Comma-separated stems to extract: vocals, drums, bass, other, guitar, piano. Default: vocals,other'))
|
|
554
|
+
.option('--model [model]', z
|
|
555
|
+
.string()
|
|
556
|
+
.describe('Demucs model variant. Default: htdemucs_6s. Options: htdemucs, htdemucs_ft, htdemucs_6s, hdemucs_mmi, mdx, mdx_extra, mdx_q, mdx_extra_q'))
|
|
557
|
+
.option('--output-format [format]', z
|
|
558
|
+
.enum(['mp3', 'wav'])
|
|
559
|
+
.default('mp3')
|
|
560
|
+
.describe('Output audio format: mp3 or wav'))
|
|
561
|
+
.option('-o, --output [dir]', z
|
|
562
|
+
.string()
|
|
563
|
+
.describe('Output directory for separated stems. Default: current directory'))
|
|
564
|
+
.option('--shifts [n]', z
|
|
565
|
+
.number()
|
|
566
|
+
.describe('Number of random shifts for quality (higher = better but slower). Default: 1'))
|
|
567
|
+
.option('--overlap [n]', z
|
|
568
|
+
.number()
|
|
569
|
+
.describe('Segment overlap 0.0-1.0 (higher = better quality). Default: 0.25'))
|
|
570
|
+
.option('--stdin', 'Read audio from stdin instead of a file path')
|
|
571
|
+
.option('--json', 'Output result metadata as JSON to stdout')
|
|
572
|
+
.example('# Separate vocals and background from an audio file')
|
|
573
|
+
.example('egaki demucs song.mp3')
|
|
574
|
+
.example('# Extract all stems (vocals, drums, bass, guitar, piano, other)')
|
|
575
|
+
.example('egaki demucs song.mp3 --stems vocals,drums,bass,other,guitar,piano')
|
|
576
|
+
.example('# Extract only vocals as WAV')
|
|
577
|
+
.example('egaki demucs song.mp3 --stems vocals --output-format wav')
|
|
578
|
+
.example('# Save to a specific directory')
|
|
579
|
+
.example('egaki demucs song.mp3 -o stems/')
|
|
580
|
+
.example('# Higher quality (slower)')
|
|
581
|
+
.example('egaki demucs song.mp3 --shifts 5')
|
|
582
|
+
.action(async (audioPath = '', options) => {
|
|
583
|
+
if (!options.stdin && !audioPath) {
|
|
584
|
+
console.error(pc.red('No audio provided. Pass an audio file path or use --stdin.'));
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
let audioData;
|
|
588
|
+
let filename;
|
|
589
|
+
if (options.stdin) {
|
|
590
|
+
const chunks = [];
|
|
591
|
+
for await (const chunk of process.stdin) {
|
|
592
|
+
chunks.push(chunk);
|
|
593
|
+
}
|
|
594
|
+
audioData = new Uint8Array(Buffer.concat(chunks));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const resolved = path.resolve(audioPath);
|
|
598
|
+
if (!fs.existsSync(resolved)) {
|
|
599
|
+
console.error(pc.red(`File not found: ${resolved}`));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
audioData = new Uint8Array(fs.readFileSync(resolved));
|
|
603
|
+
filename = path.basename(resolved);
|
|
604
|
+
}
|
|
605
|
+
if (audioData.length === 0) {
|
|
606
|
+
console.error(pc.red('No audio data provided.'));
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
const stems = options.stems
|
|
610
|
+
? options.stems.split(',').map((s) => s.trim())
|
|
611
|
+
: ['vocals', 'other'];
|
|
612
|
+
const model = (options.model ?? 'htdemucs_6s');
|
|
613
|
+
const outputFormat = options.outputFormat ?? 'mp3';
|
|
614
|
+
const outputDir = options.output ? path.resolve(options.output) : process.cwd();
|
|
615
|
+
console.error(pc.dim(`Model: ${model}`));
|
|
616
|
+
console.error(pc.dim(`Stems: ${stems.join(', ')}`));
|
|
617
|
+
console.error(pc.dim(`Audio: ${options.stdin ? 'stdin' : audioPath} (${(audioData.length / 1024 / 1024).toFixed(1)} MB)`));
|
|
618
|
+
console.error(pc.cyan('Separating audio...'));
|
|
619
|
+
const result = await runProviderCall('Audio separation', () => separateAudio({
|
|
620
|
+
audio: audioData,
|
|
621
|
+
stems,
|
|
622
|
+
model,
|
|
623
|
+
outputFormat,
|
|
624
|
+
shifts: options.shifts,
|
|
625
|
+
overlap: options.overlap,
|
|
626
|
+
filename,
|
|
627
|
+
onProgress: (msg) => console.error(pc.dim(msg)),
|
|
628
|
+
}));
|
|
629
|
+
// Create output directory if needed
|
|
630
|
+
if (!fs.existsSync(outputDir)) {
|
|
631
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
632
|
+
}
|
|
633
|
+
// Download and save each stem
|
|
634
|
+
const baseName = filename
|
|
635
|
+
? path.basename(filename, path.extname(filename))
|
|
636
|
+
: 'audio';
|
|
637
|
+
const savedFiles = [];
|
|
638
|
+
for (const stem of result.stems) {
|
|
639
|
+
console.error(pc.dim(`Downloading ${stem.name}...`));
|
|
640
|
+
const bytes = await downloadStem(stem.url);
|
|
641
|
+
if (bytes instanceof Error) {
|
|
642
|
+
console.error(pc.red(`Failed to download ${stem.name}: ${bytes.message}`));
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const stemPath = path.join(outputDir, `${baseName}-${stem.name}.${outputFormat}`);
|
|
646
|
+
fs.writeFileSync(stemPath, bytes);
|
|
647
|
+
savedFiles.push({ stem: stem.name, path: stemPath, size: bytes.length });
|
|
648
|
+
console.error(pc.green(`Saved: ${stemPath} (${(bytes.length / 1024 / 1024).toFixed(1)} MB)`));
|
|
649
|
+
}
|
|
650
|
+
if (options.json) {
|
|
651
|
+
console.log(JSON.stringify({
|
|
652
|
+
model: result.model,
|
|
653
|
+
stems: savedFiles,
|
|
654
|
+
}, null, 2));
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
// ─── voice clone command ─────────────────────────────────────────────────────
|
|
658
|
+
cli
|
|
659
|
+
.command('voice clone [audio]', dedent `
|
|
660
|
+
Clone a voice from an audio clip. Returns a voice ID for use with
|
|
661
|
+
'egaki speech --voice <id>'. Supports Cartesia (default) and ElevenLabs.
|
|
662
|
+
|
|
663
|
+
Best practices for high-quality clones:
|
|
664
|
+
|
|
665
|
+
1. Isolate vocals first: 'egaki demucs recording.mp3 --stems vocals'
|
|
666
|
+
removes background music, noise, and other speakers.
|
|
667
|
+
2. Find a clean snippet: 'egaki transcribe recording-vocals.mp3' to get
|
|
668
|
+
word timestamps. Pick a 5-10s segment with a complete phrase, clear
|
|
669
|
+
speech, no hesitations or crosstalk.
|
|
670
|
+
3. Trim to speech boundaries:
|
|
671
|
+
ffmpeg -i recording-vocals.mp3 -ss 12.5 -to 22.0 -c copy clip.mp3
|
|
672
|
+
No silence padding at start or end.
|
|
673
|
+
4. Match energy to intent: the clone mimics the tone and pacing of the
|
|
674
|
+
source clip. Use an energetic clip for energetic output.
|
|
675
|
+
5. Speak in the target language. Use --language for Cartesia clones.
|
|
676
|
+
|
|
677
|
+
Cartesia: up to 10s of audio, instant, free. Good for short clean clips.
|
|
678
|
+
ElevenLabs: 1-3 min recommended, has --remove-background-noise option.
|
|
679
|
+
`)
|
|
680
|
+
.option('--name [name]', z
|
|
681
|
+
.string()
|
|
682
|
+
.describe('Name for the cloned voice (required)'))
|
|
683
|
+
.option('-p, --provider [provider]', z
|
|
684
|
+
.enum(['cartesia', 'elevenlabs'])
|
|
685
|
+
.default('cartesia')
|
|
686
|
+
.describe('Voice cloning provider: cartesia or elevenlabs'))
|
|
687
|
+
.option('--language [lang]', z
|
|
688
|
+
.string()
|
|
689
|
+
.describe('Cartesia only: ISO 639-1 language code (default: en). E.g. en, es, fr, de, ja'))
|
|
690
|
+
.option('--description [text]', z
|
|
691
|
+
.string()
|
|
692
|
+
.describe('Optional description for the voice'))
|
|
693
|
+
.option('--base-voice-id [id]', z
|
|
694
|
+
.string()
|
|
695
|
+
.describe('Cartesia: optional base voice ID to derive from'))
|
|
696
|
+
.option('--remove-background-noise', 'ElevenLabs: apply AI noise removal to the clip before cloning')
|
|
697
|
+
.option('--stdin', 'Read audio from stdin instead of a file path')
|
|
698
|
+
.option('--json', 'Output result as JSON to stdout')
|
|
699
|
+
.example('# Clone a voice from a recording')
|
|
700
|
+
.example('egaki voice clone recording.wav --name "My Voice"')
|
|
701
|
+
.example('# Clone with ElevenLabs and noise removal')
|
|
702
|
+
.example('egaki voice clone vocals.mp3 --name "Narrator" --provider elevenlabs --remove-background-noise')
|
|
703
|
+
.example('# Full pipeline: separate → trim → clone')
|
|
704
|
+
.example('egaki demucs interview.mp3 --stems vocals')
|
|
705
|
+
.example('ffmpeg -i interview-vocals.mp3 -ss 5.0 -to 15.0 -c copy clip.mp3')
|
|
706
|
+
.example('egaki voice clone clip.mp3 --name "Speaker"')
|
|
707
|
+
.example('# Use the cloned voice')
|
|
708
|
+
.example('egaki speech "Hello world" --voice <voice-id>')
|
|
709
|
+
.action(async (audioPath = '', options) => {
|
|
710
|
+
if (!options.stdin && !audioPath) {
|
|
711
|
+
console.error(pc.red('No audio provided. Pass an audio file path or use --stdin.'));
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
const name = options.name;
|
|
715
|
+
if (!name) {
|
|
716
|
+
console.error(pc.red('Voice name is required. Use --name "My Voice".'));
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
let audioData;
|
|
720
|
+
let filename;
|
|
721
|
+
if (options.stdin) {
|
|
722
|
+
const chunks = [];
|
|
723
|
+
for await (const chunk of process.stdin) {
|
|
724
|
+
chunks.push(chunk);
|
|
725
|
+
}
|
|
726
|
+
audioData = new Uint8Array(Buffer.concat(chunks));
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
const resolved = path.resolve(audioPath);
|
|
730
|
+
if (!fs.existsSync(resolved)) {
|
|
731
|
+
console.error(pc.red(`File not found: ${resolved}`));
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
audioData = new Uint8Array(fs.readFileSync(resolved));
|
|
735
|
+
filename = path.basename(resolved);
|
|
736
|
+
}
|
|
737
|
+
if (audioData.length === 0) {
|
|
738
|
+
console.error(pc.red('No audio data provided.'));
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
const provider = (options.provider ?? 'cartesia');
|
|
742
|
+
console.error(pc.dim(`Provider: ${provider}`));
|
|
743
|
+
console.error(pc.dim(`Name: ${name}`));
|
|
744
|
+
console.error(pc.dim(`Audio: ${options.stdin ? 'stdin' : audioPath} (${(audioData.length / 1024).toFixed(1)} KB)`));
|
|
745
|
+
console.error(pc.cyan('Cloning voice...'));
|
|
746
|
+
const result = await runProviderCall('Voice cloning', () => cloneVoice({
|
|
747
|
+
audio: audioData,
|
|
748
|
+
name,
|
|
749
|
+
provider,
|
|
750
|
+
language: options.language,
|
|
751
|
+
description: options.description,
|
|
752
|
+
baseVoiceId: options.baseVoiceId,
|
|
753
|
+
removeBackgroundNoise: options.removeBackgroundNoise || false,
|
|
754
|
+
filename,
|
|
755
|
+
}));
|
|
756
|
+
console.error(pc.green(`Voice cloned successfully!`));
|
|
757
|
+
console.error(pc.bold(`Voice ID: ${result.voiceId}`));
|
|
758
|
+
console.error(pc.dim(`Use it: egaki speech "Hello" --voice ${result.voiceId}`));
|
|
759
|
+
if (options.json) {
|
|
760
|
+
console.log(JSON.stringify({
|
|
761
|
+
voiceId: result.voiceId,
|
|
762
|
+
name: result.name,
|
|
763
|
+
provider: result.provider,
|
|
764
|
+
}, null, 2));
|
|
765
|
+
}
|
|
369
766
|
});
|
|
370
767
|
// ─── models command ──────────────────────────────────────────────────────────
|
|
371
768
|
cli
|
|
@@ -375,9 +772,9 @@ cli
|
|
|
375
772
|
`)
|
|
376
773
|
.option('-p, --provider [provider]', z.string().describe('Filter models by provider name (e.g. google, openai, replicate, fal)'))
|
|
377
774
|
.option('--type [type]', z
|
|
378
|
-
.enum(['all', 'image', 'video'])
|
|
775
|
+
.enum(['all', 'image', 'video', 'speech', 'transcription'])
|
|
379
776
|
.default('all')
|
|
380
|
-
.describe('Filter by model type: image
|
|
777
|
+
.describe('Filter by model type: image, video, speech, transcription, or all'))
|
|
381
778
|
.option('--json', 'Output as JSON instead of YAML')
|
|
382
779
|
.action(async (options) => {
|
|
383
780
|
const yaml = await import('js-yaml');
|
|
@@ -386,7 +783,11 @@ cli
|
|
|
386
783
|
? [...VIDEO_CATALOG]
|
|
387
784
|
: options.type === 'image'
|
|
388
785
|
? [...CATALOG]
|
|
389
|
-
:
|
|
786
|
+
: options.type === 'speech'
|
|
787
|
+
? [...SPEECH_CATALOG]
|
|
788
|
+
: options.type === 'transcription'
|
|
789
|
+
? [...TRANSCRIPTION_CATALOG]
|
|
790
|
+
: [...CATALOG, ...VIDEO_CATALOG, ...SPEECH_CATALOG, ...TRANSCRIPTION_CATALOG];
|
|
390
791
|
if (options.provider) {
|
|
391
792
|
models = models.filter((m) => m.provider === options.provider);
|
|
392
793
|
if (models.length === 0) {
|
|
@@ -416,14 +817,34 @@ cli
|
|
|
416
817
|
? `${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}`
|
|
417
818
|
: 'unknown',
|
|
418
819
|
}
|
|
419
|
-
:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
820
|
+
: m.strategy === 'speech'
|
|
821
|
+
? {
|
|
822
|
+
outputFormats: m.features.outputFormats.join(', '),
|
|
823
|
+
instructions: m.features.instructions,
|
|
824
|
+
speed: m.features.speed,
|
|
825
|
+
language: m.features.language,
|
|
826
|
+
maxChars: m.features.maxChars || 'unlimited',
|
|
827
|
+
voices: m.features.defaultVoices.length > 0
|
|
828
|
+
? m.features.defaultVoices.join(', ')
|
|
829
|
+
: 'use provider voice library',
|
|
830
|
+
}
|
|
831
|
+
: m.strategy === 'transcription'
|
|
832
|
+
? {
|
|
833
|
+
wordTimestamps: m.features.wordTimestamps,
|
|
834
|
+
diarization: m.features.diarization,
|
|
835
|
+
languageDetection: m.features.languageDetection,
|
|
836
|
+
languageHint: m.features.languageHint,
|
|
837
|
+
inputFormats: m.features.inputFormats.join(', '),
|
|
838
|
+
maxDurationSec: m.features.maxDurationSec || 'unlimited',
|
|
839
|
+
}
|
|
840
|
+
: {
|
|
841
|
+
editing: m.features.editing,
|
|
842
|
+
inpainting: m.features.inpainting,
|
|
843
|
+
seed: m.features.seed,
|
|
844
|
+
multipleImages: m.features.multipleImages,
|
|
845
|
+
aspectRatios: m.features.aspectRatios.join(', ') || 'none',
|
|
846
|
+
...(m.features.sizes ? { sizes: m.features.sizes.join(', ') } : {}),
|
|
847
|
+
},
|
|
427
848
|
}));
|
|
428
849
|
if (options.json) {
|
|
429
850
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -438,12 +859,22 @@ const UPLOAD_GATEWAY_BASE = 'https://egaki.org';
|
|
|
438
859
|
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
439
860
|
cli.help();
|
|
440
861
|
cli.version(pkg.version);
|
|
441
|
-
|
|
862
|
+
export { cli };
|
|
442
863
|
async function runProviderCall(operation, call) {
|
|
443
864
|
const result = await call().catch((error) => {
|
|
444
865
|
printErrorDetails(error, operation);
|
|
445
866
|
process.exit(1);
|
|
446
867
|
});
|
|
868
|
+
if (result instanceof Error) {
|
|
869
|
+
// Validation errors get a clean one-line message; provider/runtime errors
|
|
870
|
+
// get full details (status code, response body, stack trace, etc.).
|
|
871
|
+
if (result instanceof ValidationError) {
|
|
872
|
+
console.error(pc.red(result.message));
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
printErrorDetails(result, operation);
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
447
878
|
return result;
|
|
448
879
|
}
|
|
449
880
|
function printErrorDetails(error, operation) {
|
|
@@ -483,6 +914,14 @@ function printErrorDetails(error, operation) {
|
|
|
483
914
|
if (error instanceof Error) {
|
|
484
915
|
printKeyValue('name', error.name);
|
|
485
916
|
printKeyValue('message', error.message);
|
|
917
|
+
// @ai-sdk/xai discards statusResponse.error when status==='failed',
|
|
918
|
+
// so XAI_VIDEO_GENERATION_FAILED carries no useful info. Add a hint.
|
|
919
|
+
if (error.name === 'XAI_VIDEO_GENERATION_FAILED') {
|
|
920
|
+
console.error(pc.dim('hint: xAI returned status "failed" but @ai-sdk/xai discards the error details.\n' +
|
|
921
|
+
'Common causes: invalid/corrupt input image, content policy violation, or transient backend error.\n' +
|
|
922
|
+
'To see the real error, poll the video status endpoint manually:\n' +
|
|
923
|
+
' curl https://api.x.ai/v1/videos/<request_id> -H "Authorization: Bearer $TOKEN"'));
|
|
924
|
+
}
|
|
486
925
|
printJsonBlock('details', serializeOwnProperties(error));
|
|
487
926
|
printCause(error.cause);
|
|
488
927
|
printStack(error);
|
|
@@ -573,7 +1012,7 @@ const FEATURED_IMAGE_MODELS = [
|
|
|
573
1012
|
'flux-pro-1.1-ultra',
|
|
574
1013
|
'recraft-v4',
|
|
575
1014
|
'dall-e-3',
|
|
576
|
-
'grok-imagine-image-
|
|
1015
|
+
'grok-imagine-image-quality',
|
|
577
1016
|
];
|
|
578
1017
|
/**
|
|
579
1018
|
* Resolve the image model: show interactive picker in TTY, use default otherwise.
|
|
@@ -634,6 +1073,66 @@ async function resolveVideoModel() {
|
|
|
634
1073
|
}
|
|
635
1074
|
return selected;
|
|
636
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Resolve the speech model: show interactive picker in TTY, use default otherwise.
|
|
1078
|
+
*/
|
|
1079
|
+
async function resolveSpeechModel() {
|
|
1080
|
+
const isTTY = process.stdin.isTTY && process.stderr.isTTY;
|
|
1081
|
+
if (!isTTY)
|
|
1082
|
+
return DEFAULT_SPEECH_MODEL;
|
|
1083
|
+
const options = SPEECH_CATALOG.map((m) => {
|
|
1084
|
+
const cost = pc.dim(`$${m.cost.perMillionChars}/M chars`);
|
|
1085
|
+
return {
|
|
1086
|
+
value: m.id,
|
|
1087
|
+
label: `${m.name} ${cost}`,
|
|
1088
|
+
hint: m.id,
|
|
1089
|
+
};
|
|
1090
|
+
});
|
|
1091
|
+
const selected = await select({
|
|
1092
|
+
message: 'Select a speech model',
|
|
1093
|
+
options,
|
|
1094
|
+
});
|
|
1095
|
+
if (isCancel(selected)) {
|
|
1096
|
+
cancel('Cancelled.');
|
|
1097
|
+
process.exit(0);
|
|
1098
|
+
}
|
|
1099
|
+
return selected;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Resolve the transcription model: show interactive picker in TTY, use default otherwise.
|
|
1103
|
+
*/
|
|
1104
|
+
async function resolveTranscriptionModel() {
|
|
1105
|
+
const isTTY = process.stdin.isTTY && process.stderr.isTTY;
|
|
1106
|
+
if (!isTTY)
|
|
1107
|
+
return DEFAULT_TRANSCRIPTION_MODEL;
|
|
1108
|
+
const options = TRANSCRIPTION_CATALOG.map((m) => {
|
|
1109
|
+
const cost = pc.dim(`${formatCost(m.cost.perSecond * 60)}/min`);
|
|
1110
|
+
return {
|
|
1111
|
+
value: m.id,
|
|
1112
|
+
label: `${m.name} ${cost}`,
|
|
1113
|
+
hint: m.id,
|
|
1114
|
+
};
|
|
1115
|
+
});
|
|
1116
|
+
const selected = await select({
|
|
1117
|
+
message: 'Select a transcription model',
|
|
1118
|
+
options,
|
|
1119
|
+
});
|
|
1120
|
+
if (isCancel(selected)) {
|
|
1121
|
+
cancel('Cancelled.');
|
|
1122
|
+
process.exit(0);
|
|
1123
|
+
}
|
|
1124
|
+
return selected;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Read text from stdin (for piping: cat file.txt | egaki speech --stdin).
|
|
1128
|
+
*/
|
|
1129
|
+
async function readTextFromStdin() {
|
|
1130
|
+
const chunks = [];
|
|
1131
|
+
for await (const chunk of process.stdin) {
|
|
1132
|
+
chunks.push(chunk);
|
|
1133
|
+
}
|
|
1134
|
+
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
1135
|
+
}
|
|
637
1136
|
function isUrl(input) {
|
|
638
1137
|
return /^https?:\/\//i.test(input);
|
|
639
1138
|
}
|
|
@@ -648,6 +1147,13 @@ function mimeTypeFromExtension(ext) {
|
|
|
648
1147
|
'.jpeg': 'image/jpeg',
|
|
649
1148
|
'.webp': 'image/webp',
|
|
650
1149
|
'.gif': 'image/gif',
|
|
1150
|
+
'.mp3': 'audio/mpeg',
|
|
1151
|
+
'.wav': 'audio/wav',
|
|
1152
|
+
'.pcm': 'audio/pcm',
|
|
1153
|
+
'.opus': 'audio/opus',
|
|
1154
|
+
'.aac': 'audio/aac',
|
|
1155
|
+
'.flac': 'audio/flac',
|
|
1156
|
+
'.ogg': 'audio/ogg',
|
|
651
1157
|
};
|
|
652
1158
|
return map[ext.toLowerCase()] || 'application/octet-stream';
|
|
653
1159
|
}
|
|
@@ -718,429 +1224,7 @@ async function readInputImages(inputs) {
|
|
|
718
1224
|
}
|
|
719
1225
|
return Promise.all(inputs.map((f) => readInputSource(f)));
|
|
720
1226
|
}
|
|
721
|
-
|
|
722
|
-
switch (provider) {
|
|
723
|
-
case 'google': {
|
|
724
|
-
const googleOpts = {
|
|
725
|
-
...(opts.allowPeople ? { personGeneration: 'allow_all' } : {}),
|
|
726
|
-
...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
|
|
727
|
-
};
|
|
728
|
-
return { google: googleOpts };
|
|
729
|
-
}
|
|
730
|
-
case 'vertex': {
|
|
731
|
-
const vertexOpts = {
|
|
732
|
-
...(opts.allowPeople ? { personGeneration: 'allow_all' } : {}),
|
|
733
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
734
|
-
};
|
|
735
|
-
return { vertex: vertexOpts };
|
|
736
|
-
}
|
|
737
|
-
case 'xai': {
|
|
738
|
-
const xaiOpts = {
|
|
739
|
-
...(opts.quality ? { quality: opts.quality } : {}),
|
|
740
|
-
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
741
|
-
...(opts.outputFormat ? { output_format: opts.outputFormat } : {}),
|
|
742
|
-
};
|
|
743
|
-
return { xai: xaiOpts };
|
|
744
|
-
}
|
|
745
|
-
case 'openai': {
|
|
746
|
-
const openaiOpts = {
|
|
747
|
-
...(opts.quality ? { quality: opts.quality } : {}),
|
|
748
|
-
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
749
|
-
};
|
|
750
|
-
return { openai: openaiOpts };
|
|
751
|
-
}
|
|
752
|
-
case 'fal': {
|
|
753
|
-
const falOpts = {
|
|
754
|
-
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
755
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
756
|
-
};
|
|
757
|
-
return { fal: falOpts };
|
|
758
|
-
}
|
|
759
|
-
default:
|
|
760
|
-
// Providers routed through the gateway (bfl, recraft, etc.) pass options
|
|
761
|
-
// as-is since they don't have local SDK types.
|
|
762
|
-
return {};
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
function buildVideoProviderOptions(provider, opts) {
|
|
766
|
-
switch (provider) {
|
|
767
|
-
case 'xai': {
|
|
768
|
-
// xAI video modes are a discriminated union; build the correct variant.
|
|
769
|
-
const base = {
|
|
770
|
-
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
771
|
-
};
|
|
772
|
-
if (opts.mode === 'edit-video' && opts.videoUrl) {
|
|
773
|
-
const xaiOpts = { ...base, mode: 'edit-video', videoUrl: opts.videoUrl };
|
|
774
|
-
return { xai: xaiOpts };
|
|
775
|
-
}
|
|
776
|
-
if (opts.mode === 'extend-video' && opts.videoUrl) {
|
|
777
|
-
const xaiOpts = { ...base, mode: 'extend-video', videoUrl: opts.videoUrl };
|
|
778
|
-
return { xai: xaiOpts };
|
|
779
|
-
}
|
|
780
|
-
if (opts.mode === 'reference-to-video' && opts.referenceImages) {
|
|
781
|
-
const xaiOpts = { ...base, mode: 'reference-to-video', referenceImageUrls: opts.referenceImages };
|
|
782
|
-
return { xai: xaiOpts };
|
|
783
|
-
}
|
|
784
|
-
// Default: text-to-video or image-to-video (no mode)
|
|
785
|
-
if (Object.keys(base).length === 0)
|
|
786
|
-
return undefined;
|
|
787
|
-
const xaiOpts = { ...base };
|
|
788
|
-
return { xai: xaiOpts };
|
|
789
|
-
}
|
|
790
|
-
case 'fal': {
|
|
791
|
-
const falOpts = {
|
|
792
|
-
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
793
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
794
|
-
};
|
|
795
|
-
if (Object.keys(falOpts).length === 0)
|
|
796
|
-
return undefined;
|
|
797
|
-
return { fal: falOpts };
|
|
798
|
-
}
|
|
799
|
-
case 'google': {
|
|
800
|
-
const googleOpts = {
|
|
801
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
802
|
-
};
|
|
803
|
-
if (Object.keys(googleOpts).length === 0)
|
|
804
|
-
return undefined;
|
|
805
|
-
return { google: googleOpts };
|
|
806
|
-
}
|
|
807
|
-
case 'vertex': {
|
|
808
|
-
const vertexOpts = {
|
|
809
|
-
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
810
|
-
};
|
|
811
|
-
if (Object.keys(vertexOpts).length === 0)
|
|
812
|
-
return undefined;
|
|
813
|
-
return { vertex: vertexOpts };
|
|
814
|
-
}
|
|
815
|
-
default:
|
|
816
|
-
return undefined;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
// Generate using the dedicated generateImage API (Imagen models)
|
|
820
|
-
async function generateWithImageModel({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, allowPeople, quality, resolution, outputFormat, negativePrompt, json, stdout, }) {
|
|
821
|
-
// Build the prompt: plain string or multimodal with reference images
|
|
822
|
-
const imagePrompt = inputImages.length > 0
|
|
823
|
-
? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
|
|
824
|
-
: prompt;
|
|
825
|
-
const config = getModelConfig(model);
|
|
826
|
-
const imageModel = await createImageModel(model);
|
|
827
|
-
// Build type-safe provider options using the SDK types with `satisfies`.
|
|
828
|
-
// Each provider has its own shape; we switch on provider to get compile-time safety.
|
|
829
|
-
const providerOptions = buildImageProviderOptions(config.provider, {
|
|
830
|
-
aspectRatio,
|
|
831
|
-
allowPeople,
|
|
832
|
-
quality,
|
|
833
|
-
resolution,
|
|
834
|
-
outputFormat,
|
|
835
|
-
negativePrompt,
|
|
836
|
-
});
|
|
837
|
-
if (!stdout) {
|
|
838
|
-
console.error(pc.cyan('Generating...'));
|
|
839
|
-
}
|
|
840
|
-
const result = await runProviderCall('Image generation', () => aiGenerateImage({
|
|
841
|
-
model: imageModel,
|
|
842
|
-
prompt: imagePrompt,
|
|
843
|
-
n: count,
|
|
844
|
-
...(aspectRatio ? { aspectRatio } : {}),
|
|
845
|
-
...(seed !== undefined ? { seed } : {}),
|
|
846
|
-
providerOptions,
|
|
847
|
-
}));
|
|
848
|
-
const files = result.images.map((img) => ({
|
|
849
|
-
uint8Array: img.uint8Array,
|
|
850
|
-
mediaType: img.mediaType,
|
|
851
|
-
}));
|
|
852
|
-
if (stdout) {
|
|
853
|
-
writeFirstToStdout(files);
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
const savedFiles = saveGeneratedFiles(files, outputPath);
|
|
857
|
-
const cost = calculateCost(config.cost, result.usage, result.images.length);
|
|
858
|
-
printCost(cost);
|
|
859
|
-
if (json) {
|
|
860
|
-
console.log(JSON.stringify({
|
|
861
|
-
model,
|
|
862
|
-
files: savedFiles,
|
|
863
|
-
count: result.images.length,
|
|
864
|
-
cost,
|
|
865
|
-
usage: result.usage,
|
|
866
|
-
warnings: result.warnings,
|
|
867
|
-
}, null, 2));
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
// Generate using generateText with responseModalities (Gemini multimodal models)
|
|
871
|
-
async function generateWithTextModel({ prompt, model, outputPath, inputImages, imageSize, aspectRatio, json, stdout, }) {
|
|
872
|
-
const textModel = await createTextModel(model);
|
|
873
|
-
const config = getModelConfig(model);
|
|
874
|
-
if (!stdout) {
|
|
875
|
-
console.error(pc.cyan('Generating...'));
|
|
876
|
-
}
|
|
877
|
-
// Build messages: if we have input images, create a multimodal prompt
|
|
878
|
-
const messages = inputImages.length > 0
|
|
879
|
-
? [
|
|
880
|
-
{
|
|
881
|
-
role: 'user',
|
|
882
|
-
content: [
|
|
883
|
-
{ type: 'text', text: prompt },
|
|
884
|
-
...inputImages.map((img) => ({
|
|
885
|
-
type: 'image',
|
|
886
|
-
image: img,
|
|
887
|
-
})),
|
|
888
|
-
],
|
|
889
|
-
},
|
|
890
|
-
]
|
|
891
|
-
: undefined;
|
|
892
|
-
const googleOpts = {
|
|
893
|
-
responseModalities: ['TEXT', 'IMAGE'],
|
|
894
|
-
...(imageSize || aspectRatio
|
|
895
|
-
? {
|
|
896
|
-
imageConfig: {
|
|
897
|
-
...(imageSize ? { imageSize } : {}),
|
|
898
|
-
...(aspectRatio ? { aspectRatio: aspectRatio } : {}),
|
|
899
|
-
},
|
|
900
|
-
}
|
|
901
|
-
: {}),
|
|
902
|
-
};
|
|
903
|
-
const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
|
|
904
|
-
const result = await runProviderCall('Text/image generation', () => generateText({
|
|
905
|
-
model: textModel,
|
|
906
|
-
...(messages ? { messages } : { prompt }),
|
|
907
|
-
providerOptions: {
|
|
908
|
-
[providerOptionsKey]: googleOpts,
|
|
909
|
-
},
|
|
910
|
-
}));
|
|
911
|
-
const imageFiles = result.files.filter((f) => f.mediaType.startsWith('image/'));
|
|
912
|
-
if (imageFiles.length === 0) {
|
|
913
|
-
console.error(pc.red('No images generated.'));
|
|
914
|
-
process.exit(1);
|
|
915
|
-
}
|
|
916
|
-
if (stdout) {
|
|
917
|
-
writeFirstToStdout(imageFiles);
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
const savedFiles = saveGeneratedFiles(imageFiles, outputPath);
|
|
921
|
-
const cost = calculateCost(config.cost, result.usage, imageFiles.length);
|
|
922
|
-
printCost(cost);
|
|
923
|
-
if (result.text && !json) {
|
|
924
|
-
console.error(pc.dim(result.text));
|
|
925
|
-
}
|
|
926
|
-
if (json) {
|
|
927
|
-
console.log(JSON.stringify({
|
|
928
|
-
model,
|
|
929
|
-
files: savedFiles,
|
|
930
|
-
count: imageFiles.length,
|
|
931
|
-
text: result.text || null,
|
|
932
|
-
cost,
|
|
933
|
-
usage: result.usage,
|
|
934
|
-
}, null, 2));
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
// Generate using the OpenAI Responses API with imageGeneration tool.
|
|
938
|
-
// Used when auth comes from ChatGPT OAuth (no Image API scope).
|
|
939
|
-
async function generateWithResponsesApi({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, json, stdout, }) {
|
|
940
|
-
if (count !== 1) {
|
|
941
|
-
console.error(pc.red('ChatGPT image generation currently supports exactly one output image.'));
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
if (seed !== undefined) {
|
|
945
|
-
console.error(pc.red('ChatGPT image generation does not support --seed.'));
|
|
946
|
-
process.exit(1);
|
|
947
|
-
}
|
|
948
|
-
if (maskImage) {
|
|
949
|
-
console.error(pc.red('ChatGPT image generation does not support --mask yet.'));
|
|
950
|
-
process.exit(1);
|
|
951
|
-
}
|
|
952
|
-
const size = aspectRatio ? chatGptImageSizeFromAspectRatio(aspectRatio) : undefined;
|
|
953
|
-
if (aspectRatio && !size) {
|
|
954
|
-
console.error(pc.red('ChatGPT image generation only supports --aspect-ratio 1:1, 3:2, or 2:3.'));
|
|
955
|
-
process.exit(1);
|
|
956
|
-
}
|
|
957
|
-
const storedAuth = getChatGptAuth();
|
|
958
|
-
if (!storedAuth?.accountId) {
|
|
959
|
-
console.error(pc.red('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.'));
|
|
960
|
-
process.exit(1);
|
|
961
|
-
}
|
|
962
|
-
const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth);
|
|
963
|
-
if (auth instanceof Error) {
|
|
964
|
-
console.error(pc.red(auth.message));
|
|
965
|
-
process.exit(1);
|
|
966
|
-
}
|
|
967
|
-
if (!stdout) {
|
|
968
|
-
console.error(pc.cyan('Generating...'));
|
|
969
|
-
}
|
|
970
|
-
const content = [
|
|
971
|
-
{ type: 'input_text', text: prompt },
|
|
972
|
-
...inputImages.map((image) => ({
|
|
973
|
-
type: 'input_image',
|
|
974
|
-
image_url: imageBytesToDataUrl(image),
|
|
975
|
-
})),
|
|
976
|
-
];
|
|
977
|
-
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
978
|
-
method: 'POST',
|
|
979
|
-
headers: {
|
|
980
|
-
Authorization: `Bearer ${auth.access}`,
|
|
981
|
-
'ChatGPT-Account-ID': auth.accountId,
|
|
982
|
-
'Content-Type': 'application/json',
|
|
983
|
-
},
|
|
984
|
-
body: JSON.stringify({
|
|
985
|
-
model: 'gpt-5.4',
|
|
986
|
-
instructions: 'You are Codex.',
|
|
987
|
-
input: [
|
|
988
|
-
{
|
|
989
|
-
type: 'message',
|
|
990
|
-
role: 'user',
|
|
991
|
-
content,
|
|
992
|
-
},
|
|
993
|
-
],
|
|
994
|
-
tools: [
|
|
995
|
-
{
|
|
996
|
-
type: 'image_generation',
|
|
997
|
-
model,
|
|
998
|
-
size: 'auto',
|
|
999
|
-
quality: 'auto',
|
|
1000
|
-
output_format: 'png',
|
|
1001
|
-
output_compression: 100,
|
|
1002
|
-
moderation: 'auto',
|
|
1003
|
-
...(size ? { size } : {}),
|
|
1004
|
-
},
|
|
1005
|
-
],
|
|
1006
|
-
tool_choice: 'auto',
|
|
1007
|
-
parallel_tool_calls: true,
|
|
1008
|
-
stream: true,
|
|
1009
|
-
store: false,
|
|
1010
|
-
include: [],
|
|
1011
|
-
}),
|
|
1012
|
-
});
|
|
1013
|
-
if (!response.ok || !response.body) {
|
|
1014
|
-
const body = await response.text().catch(() => '');
|
|
1015
|
-
console.error(pc.red(`ChatGPT image generation failed: ${response.status}`));
|
|
1016
|
-
if (body)
|
|
1017
|
-
console.error(pc.dim(body));
|
|
1018
|
-
process.exit(1);
|
|
1019
|
-
}
|
|
1020
|
-
let imageBase64;
|
|
1021
|
-
let revisedPrompt = null;
|
|
1022
|
-
const processSseMessage = (message) => {
|
|
1023
|
-
if (!message.data)
|
|
1024
|
-
return;
|
|
1025
|
-
try {
|
|
1026
|
-
const event = JSON.parse(message.data);
|
|
1027
|
-
if (event.type === 'response.image_generation_call.partial_image' &&
|
|
1028
|
-
event.partial_image_b64 &&
|
|
1029
|
-
!imageBase64) {
|
|
1030
|
-
imageBase64 = event.partial_image_b64;
|
|
1031
|
-
}
|
|
1032
|
-
if (event.type === 'response.output_item.done' &&
|
|
1033
|
-
event.item?.type === 'image_generation_call') {
|
|
1034
|
-
if (event.item.result)
|
|
1035
|
-
imageBase64 = event.item.result;
|
|
1036
|
-
revisedPrompt = event.item.revised_prompt ?? revisedPrompt;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
catch {
|
|
1040
|
-
// Ignore keepalive and partial parse noise.
|
|
1041
|
-
}
|
|
1042
|
-
};
|
|
1043
|
-
const parser = createParser({
|
|
1044
|
-
onEvent: processSseMessage,
|
|
1045
|
-
});
|
|
1046
|
-
const decoder = new TextDecoder();
|
|
1047
|
-
for await (const chunk of response.body) {
|
|
1048
|
-
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
1049
|
-
}
|
|
1050
|
-
parser.feed(decoder.decode());
|
|
1051
|
-
if (!imageBase64) {
|
|
1052
|
-
console.error(pc.red('No images generated.'));
|
|
1053
|
-
process.exit(1);
|
|
1054
|
-
}
|
|
1055
|
-
const imageBytes = Buffer.from(imageBase64, 'base64');
|
|
1056
|
-
const mediaType = 'image/png';
|
|
1057
|
-
if (stdout) {
|
|
1058
|
-
process.stdout.write(imageBytes);
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const savedFiles = [];
|
|
1062
|
-
const filePath = ensureExtension(outputPath, extensionFromMediaType(mediaType));
|
|
1063
|
-
fs.writeFileSync(filePath, imageBytes);
|
|
1064
|
-
console.error(pc.green(`Saved: ${filePath}`));
|
|
1065
|
-
savedFiles.push(filePath);
|
|
1066
|
-
if (revisedPrompt && !json) {
|
|
1067
|
-
console.error(pc.dim(`Revised prompt: ${revisedPrompt}`));
|
|
1068
|
-
}
|
|
1069
|
-
if (json) {
|
|
1070
|
-
const output = {
|
|
1071
|
-
model,
|
|
1072
|
-
files: savedFiles,
|
|
1073
|
-
count: 1,
|
|
1074
|
-
revisedPrompt,
|
|
1075
|
-
};
|
|
1076
|
-
console.log(JSON.stringify(output, null, 2));
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
// Generate using experimental_generateVideo (video models)
|
|
1080
|
-
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, mode, videoUrl, referenceImages, negativePrompt, json, stdout, }) {
|
|
1081
|
-
const videoModel = await createVideoModel(model);
|
|
1082
|
-
const config = getModelConfig(model);
|
|
1083
|
-
// Validate that the model supports video-url input before building options.
|
|
1084
|
-
// Only models with a 'video-url' providerOption (currently xAI) accept edit/extend mode.
|
|
1085
|
-
if (videoUrl && !config.providerOptions?.some((opt) => opt.flag === 'video-url')) {
|
|
1086
|
-
console.error(pc.red(`Model ${model} does not support video URL input for editing/extension`));
|
|
1087
|
-
process.exit(1);
|
|
1088
|
-
}
|
|
1089
|
-
// Build type-safe provider options for video generation.
|
|
1090
|
-
const providerOptions = buildVideoProviderOptions(config.provider, {
|
|
1091
|
-
resolution,
|
|
1092
|
-
mode,
|
|
1093
|
-
videoUrl,
|
|
1094
|
-
referenceImages,
|
|
1095
|
-
negativePrompt,
|
|
1096
|
-
model,
|
|
1097
|
-
});
|
|
1098
|
-
if (!stdout) {
|
|
1099
|
-
console.error(pc.cyan('Generating...'));
|
|
1100
|
-
}
|
|
1101
|
-
const result = await runProviderCall('Video generation', () => aiGenerateVideo({
|
|
1102
|
-
model: videoModel,
|
|
1103
|
-
prompt: inputImage
|
|
1104
|
-
? { image: inputImage, text: prompt }
|
|
1105
|
-
: prompt,
|
|
1106
|
-
n: count,
|
|
1107
|
-
...(aspectRatio ? { aspectRatio: aspectRatio } : {}),
|
|
1108
|
-
...(resolution ? { resolution: resolution } : {}),
|
|
1109
|
-
...(duration != null ? { duration } : {}),
|
|
1110
|
-
...(fps != null ? { fps } : {}),
|
|
1111
|
-
...(seed != null ? { seed } : {}),
|
|
1112
|
-
...(providerOptions ? { providerOptions } : {}),
|
|
1113
|
-
}));
|
|
1114
|
-
const files = result.videos.map((v) => ({
|
|
1115
|
-
uint8Array: v.uint8Array,
|
|
1116
|
-
mediaType: v.mediaType,
|
|
1117
|
-
}));
|
|
1118
|
-
if (stdout) {
|
|
1119
|
-
writeFirstToStdout(files);
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
const savedFiles = saveGeneratedFiles(files, outputPath);
|
|
1123
|
-
const cost = calculateCost(config.cost, {
|
|
1124
|
-
videosGenerated: result.videos.length,
|
|
1125
|
-
durationSeconds: duration,
|
|
1126
|
-
resolution,
|
|
1127
|
-
}, result.videos.length);
|
|
1128
|
-
printCost(cost);
|
|
1129
|
-
if (json) {
|
|
1130
|
-
console.log(JSON.stringify({
|
|
1131
|
-
model,
|
|
1132
|
-
files: savedFiles,
|
|
1133
|
-
count: result.videos.length,
|
|
1134
|
-
cost,
|
|
1135
|
-
warnings: result.warnings,
|
|
1136
|
-
responses: result.responses,
|
|
1137
|
-
}, null, 2));
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Save generated files to disk. Returns the list of saved file paths.
|
|
1142
|
-
* Handles single vs multi-file naming with index suffixes.
|
|
1143
|
-
*/
|
|
1227
|
+
// ─── shared output helpers ───────────────────────────────────────────────────
|
|
1144
1228
|
function saveGeneratedFiles(files, outputPath) {
|
|
1145
1229
|
const savedFiles = [];
|
|
1146
1230
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -1155,43 +1239,18 @@ function saveGeneratedFiles(files, outputPath) {
|
|
|
1155
1239
|
}
|
|
1156
1240
|
return savedFiles;
|
|
1157
1241
|
}
|
|
1158
|
-
/**
|
|
1159
|
-
* Print estimated cost to stderr if calculable.
|
|
1160
|
-
*/
|
|
1161
1242
|
function printCost(cost) {
|
|
1162
1243
|
if (cost != null) {
|
|
1163
1244
|
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
1164
1245
|
}
|
|
1165
1246
|
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Write the first file's raw bytes to stdout (for piping to other tools).
|
|
1168
|
-
*/
|
|
1169
1247
|
function writeFirstToStdout(files) {
|
|
1170
1248
|
const file = files[0];
|
|
1171
1249
|
if (file) {
|
|
1172
1250
|
process.stdout.write(Buffer.from(file.uint8Array));
|
|
1173
1251
|
}
|
|
1174
1252
|
}
|
|
1175
|
-
// ───
|
|
1176
|
-
function calculateCost(cost, usage, count = 1) {
|
|
1177
|
-
if (cost.type === 'per-image') {
|
|
1178
|
-
return cost.perImage * count;
|
|
1179
|
-
}
|
|
1180
|
-
if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
|
|
1181
|
-
return ((usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000);
|
|
1182
|
-
}
|
|
1183
|
-
if (cost.type === 'per-video-second') {
|
|
1184
|
-
const durationSec = usage.durationSeconds ?? cost.defaultDurationSec;
|
|
1185
|
-
const resolution = normalizeResolutionKey(usage.resolution);
|
|
1186
|
-
const tier = cost.tiers.find((t) => normalizeResolutionKey(t.resolution) === resolution) ??
|
|
1187
|
-
cost.tiers[0];
|
|
1188
|
-
if (!tier) {
|
|
1189
|
-
return null;
|
|
1190
|
-
}
|
|
1191
|
-
return tier.costPerSecond * durationSec * count;
|
|
1192
|
-
}
|
|
1193
|
-
return null;
|
|
1194
|
-
}
|
|
1253
|
+
// ─── display helpers ─────────────────────────────────────────────────────────
|
|
1195
1254
|
function formatCatalogCost(cost) {
|
|
1196
1255
|
if (cost.type === 'per-image') {
|
|
1197
1256
|
return `$${cost.perImage}/image`;
|
|
@@ -1199,6 +1258,12 @@ function formatCatalogCost(cost) {
|
|
|
1199
1258
|
if (cost.type === 'per-token') {
|
|
1200
1259
|
return `$${cost.inputPerM}/M input, $${cost.outputPerM}/M output`;
|
|
1201
1260
|
}
|
|
1261
|
+
if (cost.type === 'per-character') {
|
|
1262
|
+
return `$${cost.perMillionChars}/M chars`;
|
|
1263
|
+
}
|
|
1264
|
+
if (cost.type === 'per-second') {
|
|
1265
|
+
return `${formatCost(cost.perSecond * 60)}/min`;
|
|
1266
|
+
}
|
|
1202
1267
|
if (cost.type === 'per-video-second') {
|
|
1203
1268
|
const tierText = cost.tiers
|
|
1204
1269
|
.map((t) => {
|
|
@@ -1217,6 +1282,9 @@ function formatCatalogCost(cost) {
|
|
|
1217
1282
|
return 'unknown';
|
|
1218
1283
|
}
|
|
1219
1284
|
function formatCost(dollars) {
|
|
1285
|
+
if (dollars < 0.0001) {
|
|
1286
|
+
return `$${dollars.toFixed(6)}`;
|
|
1287
|
+
}
|
|
1220
1288
|
if (dollars < 0.01) {
|
|
1221
1289
|
return `$${dollars.toFixed(4)}`;
|
|
1222
1290
|
}
|
|
@@ -1231,63 +1299,21 @@ function extensionFromMediaType(mediaType) {
|
|
|
1231
1299
|
'video/mp4': '.mp4',
|
|
1232
1300
|
'video/webm': '.webm',
|
|
1233
1301
|
'video/quicktime': '.mov',
|
|
1302
|
+
'audio/mpeg': '.mp3',
|
|
1303
|
+
'audio/wav': '.wav',
|
|
1304
|
+
'audio/pcm': '.pcm',
|
|
1305
|
+
'audio/opus': '.opus',
|
|
1306
|
+
'audio/aac': '.aac',
|
|
1307
|
+
'audio/flac': '.flac',
|
|
1308
|
+
'audio/ogg': '.ogg',
|
|
1234
1309
|
};
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
return 'image/jpeg';
|
|
1243
|
-
}
|
|
1244
|
-
if (image.length >= 12 &&
|
|
1245
|
-
image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
|
|
1246
|
-
image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50) {
|
|
1247
|
-
return 'image/webp';
|
|
1248
|
-
}
|
|
1249
|
-
if (image.length >= 6 &&
|
|
1250
|
-
image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38) {
|
|
1251
|
-
return 'image/gif';
|
|
1252
|
-
}
|
|
1253
|
-
return 'image/png';
|
|
1254
|
-
}
|
|
1255
|
-
function imageBytesToDataUrl(image) {
|
|
1256
|
-
const mediaType = inferImageMediaType(image);
|
|
1257
|
-
return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`;
|
|
1258
|
-
}
|
|
1259
|
-
function chatGptImageSizeFromAspectRatio(aspectRatio) {
|
|
1260
|
-
switch (aspectRatio) {
|
|
1261
|
-
case '1:1':
|
|
1262
|
-
return '1024x1024';
|
|
1263
|
-
case '3:2':
|
|
1264
|
-
return '1536x1024';
|
|
1265
|
-
case '2:3':
|
|
1266
|
-
return '1024x1536';
|
|
1267
|
-
default:
|
|
1268
|
-
return undefined;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
function normalizeResolutionKey(input) {
|
|
1272
|
-
if (!input)
|
|
1273
|
-
return undefined;
|
|
1274
|
-
const normalized = input.trim().toLowerCase();
|
|
1275
|
-
if (normalized === '1920x1080')
|
|
1276
|
-
return '1080p';
|
|
1277
|
-
if (normalized === '1280x720')
|
|
1278
|
-
return '720p';
|
|
1279
|
-
if (normalized === '854x480' || normalized === '848x480')
|
|
1280
|
-
return '480p';
|
|
1281
|
-
return normalized;
|
|
1282
|
-
}
|
|
1283
|
-
function isAspectRatio(input) {
|
|
1284
|
-
return /^\d+:\d+$/.test(input);
|
|
1285
|
-
}
|
|
1286
|
-
function parseAspectRatioOption(input) {
|
|
1287
|
-
if (!input || !isAspectRatio(input)) {
|
|
1288
|
-
return undefined;
|
|
1289
|
-
}
|
|
1290
|
-
return input;
|
|
1310
|
+
if (map[mediaType])
|
|
1311
|
+
return map[mediaType];
|
|
1312
|
+
if (mediaType.startsWith('audio/'))
|
|
1313
|
+
return '.mp3';
|
|
1314
|
+
if (mediaType.startsWith('video/'))
|
|
1315
|
+
return '.mp4';
|
|
1316
|
+
return '.png';
|
|
1291
1317
|
}
|
|
1292
1318
|
function ensureExtension(filePath, ext) {
|
|
1293
1319
|
const parsed = path.parse(filePath);
|