egaki 0.0.2 → 0.1.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 +39 -0
- package/README.md +88 -15
- package/dist/antigravity-auth.d.ts +15 -0
- package/dist/antigravity-auth.d.ts.map +1 -0
- package/dist/antigravity-auth.js +544 -0
- package/dist/antigravity-image.d.ts +11 -0
- package/dist/antigravity-image.d.ts.map +1 -0
- package/dist/antigravity-image.js +285 -0
- package/dist/cli.js +215 -20
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.js +1 -1
- package/dist/gateway-model-candidates.generated.d.ts +11 -0
- package/dist/gateway-model-candidates.generated.d.ts.map +1 -0
- package/dist/gateway-model-candidates.generated.js +64 -0
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +169 -0
- package/dist/models.d.ts +7 -2
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +50 -1
- package/dist/open-browser.d.ts +2 -0
- package/dist/open-browser.d.ts.map +1 -0
- package/dist/open-browser.js +37 -0
- package/dist/subscription.d.ts.map +1 -1
- package/dist/subscription.js +38 -14
- package/dist/video-model-catalog.d.ts +52 -0
- package/dist/video-model-catalog.d.ts.map +1 -0
- package/dist/video-model-catalog.js +245 -0
- package/package.json +6 -6
- package/src/cli.ts +342 -23
- package/src/credentials.ts +1 -1
- package/src/model-catalog.ts +175 -0
- package/src/models.ts +64 -3
- package/src/open-browser.ts +48 -0
- package/src/subscription.ts +41 -14
- package/src/video-model-catalog.ts +302 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Antigravity image generation bridge for egaki.
|
|
2
|
+
// Handles direct Antigravity OAuth requests without provider fallback.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { getAntigravityAuth, saveAntigravityAuth } from './credentials.js';
|
|
9
|
+
import { getValidAntigravityAccessToken } from './antigravity-auth.js';
|
|
10
|
+
export async function generateWithAntigravityModel({ prompt, model, outputPath, inputImages, aspectRatio, count, json, stdout, }) {
|
|
11
|
+
const auth = getAntigravityAuth();
|
|
12
|
+
if (!auth) {
|
|
13
|
+
console.error(pc.red('Missing Antigravity OAuth login. Run: egaki login --provider antigravity'));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const token = await getValidAntigravityAccessToken(auth, (updated) => {
|
|
17
|
+
saveAntigravityAuth(updated);
|
|
18
|
+
});
|
|
19
|
+
if (token instanceof Error) {
|
|
20
|
+
console.error(pc.red(token.message));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (!stdout) {
|
|
24
|
+
console.error(pc.cyan('Generating...'));
|
|
25
|
+
}
|
|
26
|
+
const actualModel = resolveAntigravityModel(model);
|
|
27
|
+
const projectCandidates = [
|
|
28
|
+
process.env['ANTIGRAVITY_PROJECT_ID'],
|
|
29
|
+
token.projectId,
|
|
30
|
+
...getGcloudProjectIds(),
|
|
31
|
+
].filter((value, index, array) => {
|
|
32
|
+
return Boolean(value) && array.indexOf(value) === index;
|
|
33
|
+
});
|
|
34
|
+
const userParts = [{ text: prompt }];
|
|
35
|
+
for (const image of inputImages) {
|
|
36
|
+
userParts.push({
|
|
37
|
+
inlineData: {
|
|
38
|
+
mimeType: detectImageMimeType(image),
|
|
39
|
+
data: Buffer.from(image).toString('base64'),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const baseRequest = {
|
|
44
|
+
sessionId: `egaki-${crypto.randomUUID()}`,
|
|
45
|
+
systemInstruction: {
|
|
46
|
+
role: 'user',
|
|
47
|
+
parts: [{ text: 'You are an AI image generator. Generate images based on user descriptions.' }],
|
|
48
|
+
},
|
|
49
|
+
contents: [
|
|
50
|
+
{
|
|
51
|
+
role: 'user',
|
|
52
|
+
parts: userParts,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
generationConfig: {
|
|
56
|
+
candidateCount: count,
|
|
57
|
+
imageConfig: {
|
|
58
|
+
aspectRatio: aspectRatio || '1:1',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
safetySettings: [
|
|
62
|
+
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH' },
|
|
63
|
+
{ category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH' },
|
|
64
|
+
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH' },
|
|
65
|
+
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
const antigravityAttempts = projectCandidates.map((projectId) => {
|
|
69
|
+
return {
|
|
70
|
+
mode: `antigravity:${projectId}`,
|
|
71
|
+
endpoints: [
|
|
72
|
+
'https://daily-cloudcode-pa.sandbox.googleapis.com',
|
|
73
|
+
'https://autopush-cloudcode-pa.sandbox.googleapis.com',
|
|
74
|
+
'https://cloudcode-pa.googleapis.com',
|
|
75
|
+
],
|
|
76
|
+
payload: {
|
|
77
|
+
project: projectId,
|
|
78
|
+
model: actualModel,
|
|
79
|
+
requestType: 'agent',
|
|
80
|
+
userAgent: 'antigravity',
|
|
81
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
82
|
+
request: baseRequest,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
const attemptConfigs = antigravityAttempts;
|
|
87
|
+
let lastError = 'Unknown Antigravity API error';
|
|
88
|
+
let responseBody;
|
|
89
|
+
let successfulProjectId;
|
|
90
|
+
for (const attempt of attemptConfigs) {
|
|
91
|
+
for (const endpoint of attempt.endpoints) {
|
|
92
|
+
const response = await fetch(`${endpoint}/v1internal:generateContent`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
'User-Agent': 'google-api-nodejs-client/9.15.1',
|
|
98
|
+
'X-Goog-Api-Client': 'gl-node/22.17.0',
|
|
99
|
+
'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI',
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(attempt.payload),
|
|
102
|
+
});
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
lastError = formatAntigravityError({
|
|
106
|
+
mode: attempt.mode,
|
|
107
|
+
status: response.status,
|
|
108
|
+
bodyText: text,
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
responseBody = JSON.parse(text);
|
|
113
|
+
if (typeof attempt.payload.project === 'string' && attempt.payload.project.length > 0) {
|
|
114
|
+
successfulProjectId = attempt.payload.project;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (responseBody) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!responseBody) {
|
|
123
|
+
console.error(pc.red(lastError));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
if (successfulProjectId && successfulProjectId !== auth.projectId) {
|
|
127
|
+
saveAntigravityAuth({ ...auth, projectId: successfulProjectId });
|
|
128
|
+
}
|
|
129
|
+
const generatedImages = extractAntigravityImages(responseBody);
|
|
130
|
+
if (generatedImages.length === 0) {
|
|
131
|
+
console.error(pc.red('No images generated by Antigravity response'));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (stdout) {
|
|
135
|
+
process.stdout.write(generatedImages[0].bytes);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const savedFiles = [];
|
|
139
|
+
for (let i = 0; i < generatedImages.length; i++) {
|
|
140
|
+
const generated = generatedImages[i];
|
|
141
|
+
const filePath = generatedImages.length === 1
|
|
142
|
+
? ensureExtension(outputPath, extensionFromMediaType(generated.mediaType))
|
|
143
|
+
: insertIndex(outputPath, i, extensionFromMediaType(generated.mediaType));
|
|
144
|
+
fs.writeFileSync(filePath, generated.bytes);
|
|
145
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
146
|
+
savedFiles.push(filePath);
|
|
147
|
+
}
|
|
148
|
+
if (json) {
|
|
149
|
+
console.log(JSON.stringify({
|
|
150
|
+
model,
|
|
151
|
+
files: savedFiles,
|
|
152
|
+
count: savedFiles.length,
|
|
153
|
+
provider: 'antigravity',
|
|
154
|
+
projectId: token.projectId,
|
|
155
|
+
}, null, 2));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function resolveAntigravityModel(modelId) {
|
|
159
|
+
if (modelId.startsWith('antigravity-')) {
|
|
160
|
+
return modelId.slice('antigravity-'.length);
|
|
161
|
+
}
|
|
162
|
+
return modelId;
|
|
163
|
+
}
|
|
164
|
+
function getGcloudProjectIds() {
|
|
165
|
+
try {
|
|
166
|
+
const output = execFileSync('gcloud', ['projects', 'list', '--format=value(projectId)', '--limit=50'], {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
169
|
+
});
|
|
170
|
+
return output
|
|
171
|
+
.split('\n')
|
|
172
|
+
.map((line) => line.trim())
|
|
173
|
+
.filter((line) => line.length > 0);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function detectImageMimeType(bytes) {
|
|
180
|
+
if (bytes.length > 3 &&
|
|
181
|
+
bytes[0] === 0x89 &&
|
|
182
|
+
bytes[1] === 0x50 &&
|
|
183
|
+
bytes[2] === 0x4e &&
|
|
184
|
+
bytes[3] === 0x47) {
|
|
185
|
+
return 'image/png';
|
|
186
|
+
}
|
|
187
|
+
if (bytes.length > 2 && bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
188
|
+
return 'image/jpeg';
|
|
189
|
+
}
|
|
190
|
+
if (bytes.length > 12 &&
|
|
191
|
+
bytes[0] === 0x52 &&
|
|
192
|
+
bytes[1] === 0x49 &&
|
|
193
|
+
bytes[2] === 0x46 &&
|
|
194
|
+
bytes[3] === 0x46 &&
|
|
195
|
+
bytes[8] === 0x57 &&
|
|
196
|
+
bytes[9] === 0x45 &&
|
|
197
|
+
bytes[10] === 0x42 &&
|
|
198
|
+
bytes[11] === 0x50) {
|
|
199
|
+
return 'image/webp';
|
|
200
|
+
}
|
|
201
|
+
return 'image/png';
|
|
202
|
+
}
|
|
203
|
+
function extractAntigravityImages(payload) {
|
|
204
|
+
const root = payload && typeof payload === 'object' && 'response' in payload
|
|
205
|
+
? payload.response
|
|
206
|
+
: payload;
|
|
207
|
+
if (!root || typeof root !== 'object') {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
const candidates = root.candidates;
|
|
211
|
+
if (!candidates || candidates.length === 0) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
const images = [];
|
|
215
|
+
for (const candidate of candidates) {
|
|
216
|
+
for (const part of candidate.content?.parts || []) {
|
|
217
|
+
const data = part.inlineData?.data;
|
|
218
|
+
if (!data)
|
|
219
|
+
continue;
|
|
220
|
+
const mediaType = part.inlineData?.mimeType || 'image/png';
|
|
221
|
+
images.push({
|
|
222
|
+
bytes: new Uint8Array(Buffer.from(data, 'base64')),
|
|
223
|
+
mediaType,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return images;
|
|
228
|
+
}
|
|
229
|
+
function formatAntigravityError({ mode, status, bodyText, }) {
|
|
230
|
+
const payload = parseAntigravityErrorPayload(bodyText);
|
|
231
|
+
const message = payload?.error?.message || bodyText;
|
|
232
|
+
const metadata = payload?.error?.details?.find((detail) => detail.metadata)?.metadata;
|
|
233
|
+
const links = payload?.error?.details
|
|
234
|
+
?.flatMap((detail) => detail.links || [])
|
|
235
|
+
.filter((link) => Boolean(link?.url));
|
|
236
|
+
const verificationUrl = metadata?.['validation_url']
|
|
237
|
+
|| links?.find((link) => /verify/i.test(link.description || ''))?.url;
|
|
238
|
+
if (verificationUrl) {
|
|
239
|
+
return [
|
|
240
|
+
`${mode} API error ${status}: account verification required`,
|
|
241
|
+
`Verify your Google account here: ${verificationUrl}`,
|
|
242
|
+
`Backend message: ${message}`,
|
|
243
|
+
].join('\n');
|
|
244
|
+
}
|
|
245
|
+
const activationUrl = metadata?.['activationUrl']
|
|
246
|
+
|| metadata?.['activation_url']
|
|
247
|
+
|| links?.find((link) => /activation|console|api/i.test(link.description || ''))?.url;
|
|
248
|
+
if (activationUrl) {
|
|
249
|
+
return [
|
|
250
|
+
`${mode} API error ${status}: API access not configured`,
|
|
251
|
+
`Enable required API here: ${activationUrl}`,
|
|
252
|
+
`Backend message: ${message}`,
|
|
253
|
+
].join('\n');
|
|
254
|
+
}
|
|
255
|
+
return `${mode} API error ${status}: ${message}`;
|
|
256
|
+
}
|
|
257
|
+
function parseAntigravityErrorPayload(bodyText) {
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(bodyText);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function extensionFromMediaType(mediaType) {
|
|
266
|
+
const map = {
|
|
267
|
+
'image/png': '.png',
|
|
268
|
+
'image/jpeg': '.jpg',
|
|
269
|
+
'image/webp': '.webp',
|
|
270
|
+
'image/gif': '.gif',
|
|
271
|
+
};
|
|
272
|
+
return map[mediaType] || '.png';
|
|
273
|
+
}
|
|
274
|
+
function ensureExtension(filePath, ext) {
|
|
275
|
+
const parsed = path.parse(filePath);
|
|
276
|
+
if (parsed.ext) {
|
|
277
|
+
return filePath;
|
|
278
|
+
}
|
|
279
|
+
return filePath + ext;
|
|
280
|
+
}
|
|
281
|
+
function insertIndex(filePath, index, ext) {
|
|
282
|
+
const parsed = path.parse(filePath);
|
|
283
|
+
const finalExt = parsed.ext || ext;
|
|
284
|
+
return path.join(parsed.dir, `${parsed.name}-${index}${finalExt}`);
|
|
285
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
import { goke } from 'goke';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import dedent from 'string-dedent';
|
|
13
|
-
import { generateImage as aiGenerateImage, generateText } from 'ai';
|
|
13
|
+
import { generateImage as aiGenerateImage, generateText, experimental_generateVideo as aiGenerateVideo, } from 'ai';
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import pc from 'picocolors';
|
|
17
17
|
import pkg from '../package.json' with { type: 'json' };
|
|
18
18
|
import { injectCredentialsToEnv, PROVIDERS } from './credentials.js';
|
|
19
|
-
import { IMAGE_MODELS, DEFAULT_MODEL, getModelConfig, createImageModel, createTextModel, } from './models.js';
|
|
19
|
+
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, } from './models.js';
|
|
20
|
+
import { VIDEO_CATALOG } from './video-model-catalog.js';
|
|
20
21
|
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
21
22
|
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
22
23
|
const cli = goke('egaki');
|
|
@@ -87,15 +88,17 @@ cli
|
|
|
87
88
|
Egaki subscription avoids managing one key per provider.
|
|
88
89
|
Three plans: Starter ($9/mo, 100 credits), Pro ($29/mo, 500 credits),
|
|
89
90
|
Unlimited ($99/mo, 2000 credits). One credit ≈ one standard image.
|
|
90
|
-
Interactive mode: pick a plan
|
|
91
|
-
Non-interactive:
|
|
91
|
+
Interactive mode: pick a plan and get a checkout URL (email prefill optional).
|
|
92
|
+
Non-interactive: --email is optional and only pre-fills checkout.
|
|
92
93
|
`)
|
|
93
|
-
.option('-e, --email [email]', z.string().describe('
|
|
94
|
+
.option('-e, --email [email]', z.string().describe('Optional email prefill for checkout (skips interactive prompt)'))
|
|
94
95
|
.option('--plan [plan]', z.string().describe('Plan ID: starter, pro, or unlimited (default: pro)'))
|
|
95
96
|
.example('# Interactive subscribe')
|
|
96
97
|
.example('egaki subscribe')
|
|
97
98
|
.example('# Non-interactive (for agents)')
|
|
98
99
|
.example('egaki subscribe --email user@example.com --plan pro')
|
|
100
|
+
.example('# Non-interactive without email prefill')
|
|
101
|
+
.example('egaki subscribe --plan pro')
|
|
99
102
|
.action(async (options) => {
|
|
100
103
|
const isTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
101
104
|
if (!isTTY || options.email || options.plan) {
|
|
@@ -216,6 +219,73 @@ cli
|
|
|
216
219
|
});
|
|
217
220
|
}
|
|
218
221
|
});
|
|
222
|
+
// ─── video command ───────────────────────────────────────────────────────────
|
|
223
|
+
cli
|
|
224
|
+
.command('video <prompt>', dedent `
|
|
225
|
+
Generate videos from a text prompt (or image+text prompt for models that
|
|
226
|
+
support image-to-video). Uses AI SDK experimental_generateVideo under the hood.
|
|
227
|
+
|
|
228
|
+
Agent note: video generation can be slow. When invoking this command from
|
|
229
|
+
automation, use a command timeout of at least 5 minutes.
|
|
230
|
+
`)
|
|
231
|
+
.option('-m, --model [model]', z.enum(VIDEO_MODELS).default(DEFAULT_VIDEO_MODEL).describe('Video model ID for generation'))
|
|
232
|
+
.option('-o, --output [path]', z
|
|
233
|
+
.string()
|
|
234
|
+
.default('egaki-output.mp4')
|
|
235
|
+
.describe('Output file path (index suffix added when generating multiple)'))
|
|
236
|
+
.option('-n, --count [n]', z.number().default(1).describe('Number of videos to generate'))
|
|
237
|
+
.option('--aspect-ratio [ratio]', z.string().describe('Video aspect ratio in WIDTH:HEIGHT format (e.g. 16:9, 9:16)'))
|
|
238
|
+
.option('--resolution [resolution]', z
|
|
239
|
+
.string()
|
|
240
|
+
.describe('Video resolution in WIDTHxHEIGHT or provider format (e.g. 1280x720, 720p)'))
|
|
241
|
+
.option('--duration [seconds]', z.number().describe('Video duration in seconds (provider/model-specific limits apply)'))
|
|
242
|
+
.option('--fps [fps]', z.number().describe('Frames per second for video models that support fps override'))
|
|
243
|
+
.option('--seed [seed]', z.number().describe('Seed for reproducible video generation (model support varies)'))
|
|
244
|
+
.option('-i, --input [file]', z
|
|
245
|
+
.string()
|
|
246
|
+
.describe('Optional reference image for image-to-video. Accepts local file path or URL (http/https)'))
|
|
247
|
+
.option('--json', 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)')
|
|
248
|
+
.option('--stdout', 'Write raw video bytes to stdout instead of saving to a file. Useful for piping to other tools')
|
|
249
|
+
.example('# Generate a video')
|
|
250
|
+
.example('egaki video "A paper airplane gliding through clouds" -o airplane.mp4')
|
|
251
|
+
.example('# Generate with Veo model + duration')
|
|
252
|
+
.example('egaki video "cinematic rainy street at night" -m veo-3.1-fast-generate-001 --duration 6')
|
|
253
|
+
.example('# Image-to-video (model support required)')
|
|
254
|
+
.example('egaki video "animate subtle camera pan" --model luma-ray-2 --input frame.png -o animated.mp4')
|
|
255
|
+
.example('# Generate multiple videos')
|
|
256
|
+
.example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
|
|
257
|
+
.action(async (prompt, options) => {
|
|
258
|
+
injectCredentialsToEnv();
|
|
259
|
+
const model = options.model;
|
|
260
|
+
const outputPath = options.output;
|
|
261
|
+
const config = getModelConfig(model);
|
|
262
|
+
if (config.strategy !== 'video') {
|
|
263
|
+
console.error(pc.red(`Model ${model} is not a video model`));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (!options.stdout) {
|
|
267
|
+
console.error(pc.dim(`Model: ${model}`));
|
|
268
|
+
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
269
|
+
console.error(pc.dim('Mode: Video API (experimental_generateVideo)'));
|
|
270
|
+
}
|
|
271
|
+
const inputImage = options.input
|
|
272
|
+
? await readInputSource(options.input)
|
|
273
|
+
: undefined;
|
|
274
|
+
await generateWithVideoModel({
|
|
275
|
+
prompt,
|
|
276
|
+
model,
|
|
277
|
+
outputPath,
|
|
278
|
+
count: options.count,
|
|
279
|
+
aspectRatio: options.aspectRatio,
|
|
280
|
+
resolution: options.resolution,
|
|
281
|
+
duration: options.duration,
|
|
282
|
+
fps: options.fps,
|
|
283
|
+
seed: options.seed,
|
|
284
|
+
inputImage,
|
|
285
|
+
json: options.json || false,
|
|
286
|
+
stdout: options.stdout || false,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
219
289
|
// ─── models command ──────────────────────────────────────────────────────────
|
|
220
290
|
cli
|
|
221
291
|
.command('models', dedent `
|
|
@@ -223,11 +293,19 @@ cli
|
|
|
223
293
|
and provider info. Output is YAML for easy reading and piping.
|
|
224
294
|
`)
|
|
225
295
|
.option('-p, --provider [provider]', z.string().describe('Filter models by provider name (e.g. google, openai, replicate, fal)'))
|
|
296
|
+
.option('--type [type]', z
|
|
297
|
+
.enum(['all', 'image', 'video'])
|
|
298
|
+
.default('all')
|
|
299
|
+
.describe('Filter by model type: image (image+text-image), video, or all'))
|
|
226
300
|
.option('--json', 'Output as JSON instead of YAML')
|
|
227
301
|
.action(async (options) => {
|
|
228
302
|
const { CATALOG } = await import('./model-catalog.js');
|
|
229
303
|
const yaml = await import('js-yaml');
|
|
230
|
-
let models =
|
|
304
|
+
let models = options.type === 'video'
|
|
305
|
+
? VIDEO_CATALOG
|
|
306
|
+
: options.type === 'image'
|
|
307
|
+
? CATALOG
|
|
308
|
+
: [...CATALOG, ...VIDEO_CATALOG];
|
|
231
309
|
if (options.provider) {
|
|
232
310
|
models = models.filter((m) => m.provider === options.provider);
|
|
233
311
|
if (models.length === 0) {
|
|
@@ -242,17 +320,28 @@ cli
|
|
|
242
320
|
provider: m.provider,
|
|
243
321
|
strategy: m.strategy,
|
|
244
322
|
released: m.released,
|
|
245
|
-
cost: m.cost
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
323
|
+
cost: formatCatalogCost(m.cost),
|
|
324
|
+
features: m.strategy === 'video'
|
|
325
|
+
? {
|
|
326
|
+
textToVideo: m.features.textToVideo,
|
|
327
|
+
imageToVideo: m.features.imageToVideo,
|
|
328
|
+
capabilities: m.features.capabilities.join(', ') || 'none',
|
|
329
|
+
seed: m.features.seed,
|
|
330
|
+
multipleVideos: m.features.multipleVideos,
|
|
331
|
+
aspectRatios: m.features.aspectRatios?.join(', ') || 'none',
|
|
332
|
+
resolutions: m.features.resolutions?.join(', ') || 'unknown',
|
|
333
|
+
durationRangeSec: m.features.durationRangeSec
|
|
334
|
+
? `${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}`
|
|
335
|
+
: 'unknown',
|
|
336
|
+
}
|
|
337
|
+
: {
|
|
338
|
+
editing: m.features.editing,
|
|
339
|
+
inpainting: m.features.inpainting,
|
|
340
|
+
seed: m.features.seed,
|
|
341
|
+
multipleImages: m.features.multipleImages,
|
|
342
|
+
aspectRatios: m.features.aspectRatios.join(', ') || 'none',
|
|
343
|
+
...(m.features.sizes ? { sizes: m.features.sizes.join(', ') } : {}),
|
|
344
|
+
},
|
|
256
345
|
}));
|
|
257
346
|
if (options.json) {
|
|
258
347
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -432,16 +521,107 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
|
|
|
432
521
|
console.log(JSON.stringify(output, null, 2));
|
|
433
522
|
}
|
|
434
523
|
}
|
|
524
|
+
// Generate using experimental_generateVideo (video models)
|
|
525
|
+
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
|
|
526
|
+
const videoModel = await createVideoModel(model);
|
|
527
|
+
if (!stdout) {
|
|
528
|
+
console.error(pc.cyan('Generating...'));
|
|
529
|
+
}
|
|
530
|
+
const result = await aiGenerateVideo({
|
|
531
|
+
model: videoModel,
|
|
532
|
+
prompt: inputImage
|
|
533
|
+
? { image: inputImage, text: prompt }
|
|
534
|
+
: prompt,
|
|
535
|
+
n: count,
|
|
536
|
+
...(aspectRatio ? { aspectRatio: aspectRatio } : {}),
|
|
537
|
+
...(resolution ? { resolution: resolution } : {}),
|
|
538
|
+
...(duration != null ? { duration } : {}),
|
|
539
|
+
...(fps != null ? { fps } : {}),
|
|
540
|
+
...(seed != null ? { seed } : {}),
|
|
541
|
+
});
|
|
542
|
+
if (stdout) {
|
|
543
|
+
const video = result.videos[0];
|
|
544
|
+
if (video) {
|
|
545
|
+
process.stdout.write(Buffer.from(video.uint8Array));
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const savedFiles = [];
|
|
550
|
+
for (let i = 0; i < result.videos.length; i++) {
|
|
551
|
+
const video = result.videos[i];
|
|
552
|
+
const ext = extensionFromMediaType(video.mediaType);
|
|
553
|
+
const filePath = result.videos.length === 1
|
|
554
|
+
? ensureExtension(outputPath, ext)
|
|
555
|
+
: insertIndex(outputPath, i, ext);
|
|
556
|
+
fs.writeFileSync(filePath, video.uint8Array);
|
|
557
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
558
|
+
savedFiles.push(filePath);
|
|
559
|
+
}
|
|
560
|
+
const config = getModelConfig(model);
|
|
561
|
+
const cost = calculateCost(config.cost, {
|
|
562
|
+
videosGenerated: result.videos.length,
|
|
563
|
+
durationSeconds: duration,
|
|
564
|
+
resolution,
|
|
565
|
+
}, result.videos.length);
|
|
566
|
+
if (cost != null) {
|
|
567
|
+
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
568
|
+
}
|
|
569
|
+
if (json) {
|
|
570
|
+
const output = {
|
|
571
|
+
model,
|
|
572
|
+
files: savedFiles,
|
|
573
|
+
count: result.videos.length,
|
|
574
|
+
cost,
|
|
575
|
+
warnings: result.warnings,
|
|
576
|
+
responses: result.responses,
|
|
577
|
+
};
|
|
578
|
+
console.log(JSON.stringify(output, null, 2));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
435
581
|
// ─── cost helpers ────────────────────────────────────────────────────────────
|
|
436
|
-
function calculateCost(cost, usage,
|
|
582
|
+
function calculateCost(cost, usage, count = 1) {
|
|
437
583
|
if (cost.type === 'per-image') {
|
|
438
|
-
return cost.perImage *
|
|
584
|
+
return cost.perImage * count;
|
|
439
585
|
}
|
|
440
586
|
if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
|
|
441
587
|
return ((usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000);
|
|
442
588
|
}
|
|
589
|
+
if (cost.type === 'per-video-second') {
|
|
590
|
+
const durationSec = usage.durationSeconds ?? cost.defaultDurationSec;
|
|
591
|
+
const resolution = normalizeResolutionKey(usage.resolution);
|
|
592
|
+
const tier = cost.tiers.find((t) => normalizeResolutionKey(t.resolution) === resolution) ??
|
|
593
|
+
cost.tiers[0];
|
|
594
|
+
if (!tier) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return tier.costPerSecond * durationSec * count;
|
|
598
|
+
}
|
|
443
599
|
return null;
|
|
444
600
|
}
|
|
601
|
+
function formatCatalogCost(cost) {
|
|
602
|
+
if (cost.type === 'per-image') {
|
|
603
|
+
return `$${cost.perImage}/image`;
|
|
604
|
+
}
|
|
605
|
+
if (cost.type === 'per-token') {
|
|
606
|
+
return `$${cost.inputPerM}/M input, $${cost.outputPerM}/M output`;
|
|
607
|
+
}
|
|
608
|
+
if (cost.type === 'per-video-second') {
|
|
609
|
+
const tierText = cost.tiers
|
|
610
|
+
.map((t) => {
|
|
611
|
+
const parts = [
|
|
612
|
+
t.resolution,
|
|
613
|
+
t.mode ? `mode=${t.mode}` : undefined,
|
|
614
|
+
t.audio != null ? `audio=${t.audio}` : undefined,
|
|
615
|
+
].filter(Boolean);
|
|
616
|
+
return parts.length > 0
|
|
617
|
+
? `$${t.costPerSecond}/s (${parts.join(', ')})`
|
|
618
|
+
: `$${t.costPerSecond}/s`;
|
|
619
|
+
})
|
|
620
|
+
.join('; ');
|
|
621
|
+
return `${tierText} (default duration ${cost.defaultDurationSec}s)`;
|
|
622
|
+
}
|
|
623
|
+
return 'unknown';
|
|
624
|
+
}
|
|
445
625
|
function formatCost(dollars) {
|
|
446
626
|
if (dollars < 0.01) {
|
|
447
627
|
return `$${dollars.toFixed(4)}`;
|
|
@@ -454,8 +634,23 @@ function extensionFromMediaType(mediaType) {
|
|
|
454
634
|
'image/jpeg': '.jpg',
|
|
455
635
|
'image/webp': '.webp',
|
|
456
636
|
'image/gif': '.gif',
|
|
637
|
+
'video/mp4': '.mp4',
|
|
638
|
+
'video/webm': '.webm',
|
|
639
|
+
'video/quicktime': '.mov',
|
|
457
640
|
};
|
|
458
|
-
return map[mediaType] || '.png';
|
|
641
|
+
return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png');
|
|
642
|
+
}
|
|
643
|
+
function normalizeResolutionKey(input) {
|
|
644
|
+
if (!input)
|
|
645
|
+
return undefined;
|
|
646
|
+
const normalized = input.trim().toLowerCase();
|
|
647
|
+
if (normalized === '1920x1080')
|
|
648
|
+
return '1080p';
|
|
649
|
+
if (normalized === '1280x720')
|
|
650
|
+
return '720p';
|
|
651
|
+
if (normalized === '854x480' || normalized === '848x480')
|
|
652
|
+
return '480p';
|
|
653
|
+
return normalized;
|
|
459
654
|
}
|
|
460
655
|
function ensureExtension(filePath, ext) {
|
|
461
656
|
const parsed = path.parse(filePath);
|
package/dist/credentials.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare const PROVIDERS: Record<string, {
|
|
|
3
3
|
label: string;
|
|
4
4
|
hint: string;
|
|
5
5
|
}>;
|
|
6
|
-
export declare const EGAKI_GATEWAY_URL = "https://egaki.org/
|
|
6
|
+
export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
|
|
7
7
|
export type Credentials = Record<string, string>;
|
|
8
8
|
export declare function readCredentials(): Credentials;
|
|
9
9
|
export declare function writeCredentials(creds: Credentials): void;
|
package/dist/credentials.js
CHANGED
|
@@ -34,7 +34,7 @@ export const PROVIDERS = {
|
|
|
34
34
|
hint: 'Get your key at https://fal.ai/dashboard/keys',
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
-
export const EGAKI_GATEWAY_URL = 'https://egaki.org/
|
|
37
|
+
export const EGAKI_GATEWAY_URL = 'https://egaki.org/v3/ai';
|
|
38
38
|
function getConfigDir() {
|
|
39
39
|
// XDG_CONFIG_HOME or ~/.config/egaki
|
|
40
40
|
const xdg = process.env['XDG_CONFIG_HOME'];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type GatewayModelCandidate = {
|
|
2
|
+
fullId: string;
|
|
3
|
+
provider: string;
|
|
4
|
+
modelId: string;
|
|
5
|
+
type: string;
|
|
6
|
+
tags: string[];
|
|
7
|
+
reason: 'type-image' | 'tag-image-generation';
|
|
8
|
+
};
|
|
9
|
+
export declare const GATEWAY_MODEL_CANDIDATES: GatewayModelCandidate[];
|
|
10
|
+
export declare const GATEWAY_MODEL_CANDIDATE_PROVIDERS: string[];
|
|
11
|
+
//# sourceMappingURL=gateway-model-candidates.generated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-model-candidates.generated.d.ts","sourceRoot":"","sources":["../src/gateway-model-candidates.generated.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,MAAM,EAAE,YAAY,GAAG,sBAAsB,CAAA;CAC9C,CAAA;AAED,eAAO,MAAM,wBAAwB,EAAE,qBAAqB,EAwD3D,CAAA;AAED,eAAO,MAAM,iCAAiC,UAG7C,CAAA"}
|