diff --git a/app.js b/app.js index 4b5540b..6ca4d98 100644 --- a/app.js +++ b/app.js @@ -1,18 +1,12 @@ 'use strict'; -// ============================================================ -// Config -// ============================================================ - const express = require('express'); -const fetch = require('node-fetch'); const path = require('path'); const fs = require('fs'); const PORT = 8195; const HOST = '0.0.0.0'; -// Config file stored alongside app.js const CONFIG_FILE = path.join(__dirname, 'config.json'); function loadConfig() { @@ -27,22 +21,15 @@ function saveConfig(cfg) { fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2)); } -// ============================================================ -// App -// ============================================================ - const app = express(); app.use(express.json()); app.use(express.static(__dirname)); -// --- Settings --- +// ── Config ────────────────────────────────────────────────────── app.get('/api/config', (req, res) => { const cfg = loadConfig(); - res.json({ - hasApiKey: !!cfg.apiKey, - baseUrl: cfg.baseUrl || 'https://api.minimax.io', - }); + res.json({ hasApiKey: !!cfg.apiKey, baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' }); }); app.post('/api/config', (req, res) => { @@ -50,66 +37,135 @@ app.post('/api/config', (req, res) => { if (typeof apiKey !== 'string' || typeof baseUrl !== 'string') { return res.status(400).json({ error: '参数格式错误' }); } - const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimax.io' }; + const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimaxi.com' }; saveConfig(cfg); res.json({ ok: true }); }); -// --- Image generation --- +// ── Image Generation ───────────────────────────────────────────── app.post('/api/generate', async (req, res) => { - const { prompt, aspect_ratio, model } = req.body; + const { + model, prompt, style, aspect_ratio, + width, height, response_format, seed, + n, prompt_optimizer, aigc_watermark, + } = req.body; + // Validation if (!prompt || typeof prompt !== 'string' || !prompt.trim()) { - return res.status(400).json({ error: '请输入图片描述内容' }); + return res.status(400).json({ error: '请输入图片描述' }); + } + if (model !== 'image-01' && model !== 'image-01-live') { + return res.status(400).json({ error: 'model 参数无效' }); } const cfg = loadConfig(); if (!cfg.apiKey) { - return res.status(400).json({ error: '未配置 API Key,请先在设置中填写 MiniMax API Key。' }); + return res.status(400).json({ error: '未配置 API Key,请先在设置中填写。' }); } - const baseUrl = cfg.baseUrl || 'https://api.minimax.io'; - const endpoint = `${baseUrl}/v1/image_generation`; - + // Build payload — only include optional fields that are set const payload = { - model: model || 'image-01', - prompt: prompt.trim(), - aspect_ratio: aspect_ratio || '1:1', - response_format: 'base64', + model, + prompt: prompt.trim(), + response_format: response_format || 'url', }; + if (aspect_ratio) payload.aspect_ratio = aspect_ratio; + if (width && height) { payload.width = width; payload.height = height; } + if (seed) payload.seed = seed; + if (n) payload.n = Math.min(Math.max(Number(n), 1), 9); + if (prompt_optimizer) payload.prompt_optimizer = true; + if (aigc_watermark) payload.aigc_watermark = true; + + // style only for image-01-live + if (model === 'image-01-live' && style) { + const { style_type, style_weight } = style; + if (style_type) { + payload.style = { style_type }; + if (style_weight != null) payload.style.style_weight = style_weight; + } + } + + const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com'; + const endpoint = `${baseUrl}/v1/image_generation`; + + let response; try { - const response = await fetch(endpoint, { - method: 'POST', + response = await fetch(endpoint, { + method: 'POST', headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); - - const data = await response.json(); - - if (!response.ok) { - const msg = data.error?.message || data.error || `HTTP ${response.status}`; - return res.status(response.status).json({ error: msg }); - } - - // Return base64 images - const images = data.data?.image_base64 || []; - res.json({ images }); } catch (err) { - console.error('[generate] error:', err.message); - res.status(500).json({ error: '无法连接 MiniMax API:' + err.message }); + console.error('[generate] network error:', err.message); + return res.status(502).json({ error: '无法连接 MiniMax API:' + err.message }); + } + + const data = await response.json(); + + if (!response.ok) { + const code = data.base_resp?.status_code; + const msg = data.base_resp?.status_msg || data.error || `HTTP ${response.status}`; + const friendly = friendlyError(code, msg); + return res.status(response.status).json({ error: friendly }); + } + + // Return images + task id + metadata + res.json({ + images: data.data?.image_urls || data.data?.image_base64 || [], + id: data.id, + success_count: data.metadata?.success_count ?? (Array.isArray(data.data?.image_urls || data.data?.image_base64) ? 1 : 0), + failed_count: data.metadata?.failed_count ?? 0, + }); +}); + +// ── Task Status (future-proofing) ─────────────────────────────── + +app.get('/api/task/:id', async (req, res) => { + const cfg = loadConfig(); + if (!cfg.apiKey) { + return res.status(400).json({ error: '未配置 API Key。' }); + } + const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com'; + try { + const response = await fetch(`${baseUrl}/v1/image_generation/${req.params.id}`, { + headers: { 'Authorization': `Bearer ${cfg.apiKey}` }, + }); + const data = await response.json(); + if (!response.ok) { + return res.status(response.status).json({ error: data.base_resp?.status_msg || '请求失败' }); + } + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); } }); -// --- Serve index.html at root --- +// ── SPA fallback ─────────────────────────────────────────────── + app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); app.listen(PORT, HOST, () => { - console.log(`Image Generator running at http://${HOST === '0.0.0.0' ? '10.0.10.110' : HOST}:${PORT}`); + console.log(`图片生成器运行于 http://${HOST === '0.0.0.0' ? '10.0.10.110' : HOST}:${PORT}`); }); + +// ── Helpers ───────────────────────────────────────────────────── + +function friendlyError(code, msg) { + const map = { + 1002: '触发限流,请稍后再试', + 1004: '账号鉴权失败,请检查 API Key 是否正确', + 1008: '账号余额不足', + 1026: '图片描述涉及敏感内容,请修改后重试', + 2013: '参数异常,请检查输入是否合规', + 2049: '无效的 API Key', + }; + if (code && map[code]) return map[code]; + return msg; +} diff --git a/index.html b/index.html index e73bac1..2df5bb7 100644 --- a/index.html +++ b/index.html @@ -34,14 +34,28 @@ 未配置 API Key,请先在 设置 中填写。 - + + + + + image-01 + 标准模型 + + + image-01-live + 支持画风 + + + + + @@ -50,64 +64,122 @@ - - - Aspect Ratio - - 1:1 — 正方形 (1024×1024) - 16:9 — 宽屏 (1280×720) - 4:3 — 标准 (1152×864) - 3:2 — 照片 (1248×832) - 2:3 — 竖图 (832×1248) - 3:4 — 竖图 (864×1152) - 9:16 — 竖屏 (720×1280) - 21:9 — 超宽 (1344×576) - + + + + + + + + + 比例 + + 1:1 — 正方形 + 16:9 — 宽屏 + 4:3 — 标准 + 3:2 — 照片 + 2:3 — 竖图 + 3:4 — 竖图 + 9:16 — 竖屏 + 21:9 — 超宽(仅 image-01) + + + + + + 输出格式 + + URL(24小时有效) + Base64(直接保存) + + + + + + 生成数量 (1-9张) + + − + + + + + + + + + 随机种子 (留空则随机) + + + - - - - - 生成图片 - - - - - + + + + + + + 自定义分辨率 (512-2048,必须是8的倍数) + + + × + + + 同时设置宽高时优先于比例 + + + + + 画风类型 + + 漫画 + 元气 + 中世纪 + 水彩 + + 画风权重 0.8 + + + + + + + + + 自动优化 Prompt + + + + + 添加 AI 水印 + + + + + + + + + + + + 生成图片 + + + + + - + - - - - 生成结果 - - - - - - - - 下载 - - - - - - - 复制 - - - - - - + + + + - + @@ -128,12 +200,11 @@ - - + - 请从 platform.minimaxi.com 获取 API Key + 请从 platform.minimaxi.com 获取 API 地址 @@ -143,7 +214,7 @@ @@ -151,75 +222,115 @@ +
同时设置宽高时优先于比例
请从 platform.minimaxi.com 获取 API Key
请从 platform.minimaxi.com 获取