Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

181 lines
8.4 KiB
JavaScript

/**
* Yollomi API utilities — browser cookie strategy.
*
* Uses the same per-model API routes as the Yollomi frontend:
* POST /api/ai/<model> — image generation (session cookie auth)
* POST /api/ai/video — video generation (session cookie auth)
*
* Auth: browser session cookies from NextAuth — just log in to yollomi.com in Chrome.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { CliError } from '@jackwener/opencli/errors';
export const YOLLOMI_DOMAIN = 'yollomi.com';
/**
* Ensure the browser tab is on yollomi.com.
* The framework pre-nav sometimes silently fails, leaving the page on about:blank.
*/
export async function ensureOnYollomi(page) {
const currentUrl = await page.evaluate(`(() => location.href)()`);
if (!currentUrl || !currentUrl.includes('yollomi.com')) {
await page.goto('https://yollomi.com');
await page.wait(3);
}
}
/**
* POST to a Yollomi /api/ai/* route via the browser session.
* Uses relative paths (e.g. `/api/ai/flux`) — same as the frontend.
*/
export async function yollomiPost(page, apiPath, body) {
const bodyJson = JSON.stringify(body);
await ensureOnYollomi(page);
const result = await page.evaluate(`
(async () => {
try {
const res = await fetch(${JSON.stringify(apiPath)}, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: ${JSON.stringify(bodyJson)},
});
const text = await res.text();
return { ok: res.ok, status: res.status, body: text };
} catch (err) {
return { ok: false, status: 0, body: err.message || 'fetch failed (on ' + location.href + ')' };
}
})()
`);
if (!result || result.status === 0) {
throw new CliError('FETCH_ERROR', `Network error: ${result?.body || 'Failed to fetch'}`, 'Make sure Chrome is logged in to https://yollomi.com and the Browser Bridge is running');
}
if (!result.ok) {
let detail = result.body;
try {
detail = JSON.parse(result.body)?.error || JSON.parse(result.body)?.message || result.body;
}
catch { }
throw new CliError('API_ERROR', `Yollomi API ${result.status}: ${detail}`, result.status === 401
? 'Not logged in — open Chrome, go to https://yollomi.com and log in'
: result.status === 402
? 'Insufficient credits — top up at https://yollomi.com/pricing'
: result.status === 429
? 'Rate limited — wait a moment and retry'
: 'Check the model and parameters');
}
try {
return JSON.parse(result.body);
}
catch {
throw new CliError('API_ERROR', 'Invalid JSON response', 'Try again');
}
}
/**
* Resolve an image input: local file → base64 data URL, URL → as-is.
*/
export function resolveImageInput(input) {
if (input.startsWith('http://') || input.startsWith('https://') || input.startsWith('data:')) {
return input;
}
const resolved = path.resolve(input);
if (!fs.existsSync(resolved)) {
throw new CliError('FILE_NOT_FOUND', `File not found: ${resolved}`, 'Provide a valid file path or URL');
}
const ext = path.extname(resolved).toLowerCase();
const mimeMap = {
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
'.png': 'image/png', '.gif': 'image/gif',
'.webp': 'image/webp', '.bmp': 'image/bmp',
};
const mime = mimeMap[ext] || 'image/png';
const data = fs.readFileSync(resolved);
return `data:${mime};base64,${data.toString('base64')}`;
}
export async function downloadOutput(url, outputDir, filename) {
fs.mkdirSync(outputDir, { recursive: true });
const destPath = path.join(outputDir, filename);
const resp = await fetch(url);
if (!resp.ok)
throw new CliError('DOWNLOAD_ERROR', `Download failed: HTTP ${resp.status}`, 'URL may have expired');
const buffer = Buffer.from(await resp.arrayBuffer());
fs.writeFileSync(destPath, buffer);
return { path: destPath, size: buffer.length };
}
export function fmtBytes(bytes) {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}
/** Per-model API route mapping (matches frontend model.apiEndpoint). */
export const MODEL_ROUTES = {
'flux': '/api/ai/flux',
'flux-schnell': '/api/ai/flux-schnell',
'flux-2-pro': '/api/ai/flux-2-pro',
'flux-kontext-pro': '/api/ai/flux-kontext-pro',
'nano-banana': '/api/ai/nano-banana',
'nano-banana-pro': '/api/ai/nano-banana-pro',
'nano-banana-2': '/api/ai/nano-banana-2',
'z-image-turbo': '/api/ai/z-image-turbo',
'imagen-4-ultra': '/api/ai/imagen-4-ultra',
'imagen-4-fast': '/api/ai/imagen-4-fast',
'ideogram-v3-turbo': '/api/ai/ideogram-v3-turbo',
'stable-diffusion-3-5-large': '/api/ai/stable-diffusion-3-5-large',
'seedream-4-5': '/api/ai/seedream-4-5',
'seedream-5-lite': '/api/ai/seedream-5-lite',
'qwen-image-edit': '/api/ai/qwen-image-edit',
'qwen-image-edit-plus': '/api/ai/qwen-image-edit-plus',
'remove-bg': '/api/ai/remove-bg',
'image-upscaler': '/api/ai/image-upscaler',
'face-swap': '/api/ai/face-swap',
'virtual-try-on': '/api/ai/virtual-try-on',
'photo-restoration': '/api/ai/photo-restoration',
'ai-background-generator': '/api/ai/ai-background-generator',
'object-remover': '/api/ai/object-remover',
};
/** Well-known image model IDs and their credit costs. */
export const IMAGE_MODELS = {
'z-image-turbo': { credits: 1, description: 'Alibaba Qwen turbo (cheapest, 1 credit)' },
'flux-schnell': { credits: 2, description: 'High-speed Flux generation' },
'ideogram-v3-turbo': { credits: 3, description: 'Ideogram V3 Turbo' },
'imagen-4-fast': { credits: 3, description: 'Google Imagen 4 Fast' },
'seedream-4-5': { credits: 4, description: 'Seedream 4.5 (ByteDance)' },
'seedream-5-lite': { credits: 4, description: 'Seedream 5 Lite — 2K/3K' },
'flux': { credits: 4, description: 'Flux 1.1 Pro' },
'nano-banana': { credits: 4, description: 'Google Nano Banana' },
'flux-kontext-pro': { credits: 4, description: 'Flux Kontext Pro (img2img)' },
'imagen-4-ultra': { credits: 6, description: 'Google Imagen 4 Ultra' },
'nano-banana-2': { credits: 7, description: 'Google Nano Banana 2' },
'stable-diffusion-3-5-large': { credits: 7, description: 'Stable Diffusion 3.5 Large' },
'nano-banana-pro': { credits: 15, description: 'Nano Banana Pro' },
'flux-2-pro': { credits: 15, description: 'Flux 2 Pro (premium)' },
};
export const VIDEO_MODELS = {
'kling-v2-6-motion-control': { credits: 7, description: 'Kling v2.6 Motion Control' },
'bytedance-seedance-1-pro-fast': { credits: 8, description: 'Seedance 1.0 Pro Fast' },
'kling-2-1': { credits: 9, description: 'Kling 2.1' },
'minimax-hailuo-2-3': { credits: 9, description: 'Hailuo 2.3' },
'pixverse-5': { credits: 9, description: 'PixVerse 5' },
'wan-2-5-t2v': { credits: 9, description: 'Wan 2.5 Text-to-Video' },
'wan-2-5-i2v': { credits: 9, description: 'Wan 2.5 Image-to-Video' },
'google-veo-3-fast': { credits: 9, description: 'Google Veo 3 Fast' },
'google-veo-3-1-fast': { credits: 9, description: 'Google Veo 3.1 Fast' },
'openai-sora-2': { credits: 10, description: 'Sora 2' },
'google-veo-3': { credits: 10, description: 'Google Veo 3' },
'google-veo-3-1': { credits: 10, description: 'Google Veo 3.1' },
'wan-2-6-t2v': { credits: 29, description: 'Wan 2.6 T2V (premium)' },
'wan-2-6-i2v': { credits: 29, description: 'Wan 2.6 I2V (premium)' },
};
export const TOOL_MODELS = {
'remove-bg': { credits: 0, description: 'Remove background (free)' },
'image-upscaler': { credits: 1, description: 'Enhance image resolution' },
'object-remover': { credits: 3, description: 'Remove unwanted objects' },
'face-swap': { credits: 3, description: 'Swap faces in photos' },
'virtual-try-on': { credits: 3, description: 'Try clothes on photos' },
'qwen-image-edit': { credits: 3, description: 'Edit image with text prompt' },
'qwen-image-edit-plus': { credits: 3, description: 'Advanced image editing' },
'photo-restoration': { credits: 4, description: 'Revive old/damaged photos' },
'ai-background-generator': { credits: 5, description: 'Generate custom backgrounds' },
};