
Nano Banana 2 Image Gen
- 28 installs
- 33 repo stars
- Updated July 5, 2026
- wuchubuzai2018/expert-skills-hub
Helps with ai & agent building tasks.
About
nano-banana-2-image-gen is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- nano-banana-2-image-gen
- AI & Agent Building
- AI-coding skill
Nano Banana 2 Image Gen by the numbers
- 28 all-time installs (skills.sh)
- Ranked #9,505 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wuchubuzai2018/expert-skills-hub --skill nano-banana-2-image-genAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 28 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 5, 2026 |
| Repository | wuchubuzai2018/expert-skills-hub ↗ |
What it does
Helps with ai & agent building tasks.
Files
图片生成与编辑
基于谷歌的Gemini 3.1 Flash模型实现图片生成技能,可以通过自然语言帮助用户生成图片,通过API易国内代理服务访问,支持Node.js和Python两种运行环境。Nano Banana 2(代号)是谷歌于 2026 年 2 月 26 日发布的最新图像生成模型,模型 ID 为 gemini-3.1-flash-image-preview。它以 Pro 级画质 + Flash 级速度和成本 重新定义了图像生成的性价比,是 Nano Banana 系列的最新旗舰。
使用指引
遵循以下步骤:
第1步:分析需求与参数提取
1. 明确意图:区分用户是需要【文生图】(生成新图片)还是【图生图】(编辑/修改现有图片)。 2. 提示词(Prompt)分析:
- 使用用户原始完整输入:把用户输入的原始完整问题需求描述(原文)直接作为
-p提示词的主体,避免自行改写、总结或二次创作,防止细节丢失。 - 需要补充时先确认:如果信息不足(例如缺少风格、主体数量、镜头语言、场景细节、文字内容、禁止元素等),先向用户提问确认;用户确认后,再把补充内容以“追加”的方式拼接到原始提示词后。
- 样例:
- 用户输入:“帮我生成一张猫的图片,风格要可爱一点。”
- 正例说明:直接使用用户输入作为提示词:
-p "帮我生成一张猫的图片,风格要可爱一点。" - 反例说明:擅自改写为“生成一张可爱风格的猫的图片”会丢失用户原始输入的细节和语气。
- 如果需要补充细节(例如颜色、背景等),先提问确认:“你希望猫是什么颜色的?背景有什么要求吗?”用户回答后,再追加到提示词中:
-p "帮我生成一张猫的图片,风格要可爱一点。猫是橘色的,背景是草地。"
3. 关键参数整理:
- Prompt(必需):提示词分析后的最终提示词(默认=用户原始完整且一致的输入;仅在用户确认后才追加补充信息)。
- Filename(可选):输出图片文件名/路径(需包含文件随机标识,避免重复)。不传则脚本会自动生成带时间戳的文件名。建议根据内容生成合理文件名(例如
cat_in_garden.png),避免使用通用名。 - Aspect Ratio(可选):根据用户描述推断比例。例如:
- "手机壁纸" ->
9:16 - "电脑壁纸/视频封面" ->
16:9 - "头像" ->
1:1 - 默认若用户未明确不指定图片比例,保持图片比例为空。
- Resolution(可选):
- 默认图片比例使用
2K。 - 仅在极端高清需求或用户指定时使用
4K,并通过友好性提示,提示用户生成较慢,耐心等待。 - 注意:参数值必须大写(
1K,2K,4K)。
第2步:环境检查与命令执行
1. 检查环境:确认 APIYI_API_KEY 环境变量是否已设置(通常假定已设置,若运行失败再提示用户)。 2. 构建并运行命令:
- 优先尝试 Node.js 版本:如果环境有 Node(
node命令可用),优先使用scripts/generate_image.js(零依赖,参数与 Python 保持一致)。 - Node 不可用再用 Python 版本:使用
scripts/generate_image.py。
文生图命令模板(优先 Node.js):
node scripts/generate_image.js -p "{prompt}" -f "{filename}" [-a {ratio}] [-r {res}]图生图命令模板(优先 Node.js):
node scripts/generate_image.js -p "{edit_instruction}" -i "{input_path}" -f "{output_filename}" [-r {res}](可选)Python 版本命令模板(Node 不可用时):
python scripts/generate_image.py -p "{prompt}" -f "{filename}" [-a {ratio}] [-r {res}]
python scripts/generate_image.py -p "{edit_instruction}" -i "{input_path}" -f "{output_filename}" [-r {res}]⏱️ 长时间任务处理策略
1. 任务前提示
执行前必须告知用户:
- "图片生成已启动,预计需要25秒到5分钟"
2. 🎨 最佳实践示例
1. 快速生成场景(1K分辨率)
"快速模式:1K分辨率生成,预计30秒内完成"
2. 高质量生成场景(2K/4K分辨率)
"高质量模式:2K分辨率生成,预计1-4分钟\n⏳ 开始生成... 🔄"
第3步:结果反馈
1. 执行反馈:等待终端命令执行完毕。 2. 成功:告知用户图片已生成,并指出保存路径。 3. 失败:
- 若提示 API Key 缺失,请指导用户设置环境变量。
- 若提示网络错误,建议用户检查网络或稍后重试。
命令行使用样例
生成新图片
python scripts/generate_image.py -p "图片描述文本" -f "output.png" [-a 1:1] [-r 1K]示例:
# 基础生成
python scripts/generate_image.py -p "一只可爱的橘猫在草地上玩耍" -f "cat.png"
# 指定比例和分辨率
python scripts/generate_image.py -p "日落山脉风景" -f "sunset.png" -a 16:9 -r 4K
# 竖版高清图片(适合手机壁纸)
python scripts/generate_image.py -p "城市夜景" -f "city.png" -a 9:16 -r 2K(可选)Node.js 版本示例:
# 基础生成
node scripts/generate_image.js -p "一只可爱的橘猫在草地上玩耍" -f "cat.png"
# 指定比例和分辨率
node scripts/generate_image.js -p "日落山脉风景" -f "sunset.png" -a 16:9 -r 4K编辑已有图片
python scripts/generate_image.py -p "编辑指令" -f "output.png" -i "path/to/input.png" [-a 1:1] [-r 1K]示例:
# 修改风格
python scripts/generate_image.py -p "将图片转换成水彩画风格" -f "watercolor.png" -i "original.png"
# 添加元素
python scripts/generate_image.py -p "在天空添加彩虹" -f "rainbow.png" -i "landscape.png" -r 2K
# 替换背景
python scripts/generate_image.py -p "将背景换成海滩" -f "beach-bg.png" -i "portrait.png" -a 3:4(可选)Node.js 版本示例:
# 修改风格
node scripts/generate_image.js -p "将图片转换成水彩画风格" -f "watercolor.png" -i "original.png"
# 多张参考图(最多14张)
node scripts/generate_image.js -p "参考多张图片融合风格" -i ref1.png ref2.png ref3.png -f "merged.png"附加资源
- 常见使用场景文档:references/scene.md
命令行参数说明
Python 与 Node.js 版本参数保持一致(短参数与长参数等价)。
| 参数 | 必填 | 说明 |
|---|---|---|
-p / --prompt | 是 | 图片描述(文生图)或编辑指令(图生图)。保留用户原始完整输入。 |
-f / --filename | 否 | 输出图片路径/文件名;不传则自动生成带时间戳的 PNG 文件名,并写入当前目录。 |
-a / --aspect-ratio | 否 | 图片比例:1:1、16:9、9:16、4:3、3:4、3:2、2:3、5:4、4:5、1:4、4:1、1:8、8:1、21:9。 |
-r / --resolution | 否 | 图片分辨率:1K / 2K / 4K(必须大写)。不传则不在请求中指定,由 API 侧决定。 |
-i / --input-image | 否 | 图生图输入图片路径;可传多张(最多 14 张)。传入该参数即进入编辑模式。 |
图片参数说明
aspect_ratio - 图片比例
支持以下14种比例:
| 比例 | 方向 | 适用场景 |
|---|---|---|
| 1:1 | 正方形 | 头像、Instagram帖子 |
| 16:9 | 横版 | YouTube缩略图、桌面壁纸、演示文稿 |
| 9:16 | 竖版 | 抖音/TikTok、Instagram Stories、手机壁纸 |
| 4:3 | 横版 | 经典照片、演示文稿 |
| 3:4 | 竖版 | Pinterest、人像摄影 |
| 3:2 | 横版 | 单反相机标准、印刷媒体 |
| 2:3 | 竖版 | 人像海报 |
| 5:4 | 横版 | 大幅面打印、艺术印刷 |
| 4:5 | 竖版 | Instagram帖子、社交媒体 |
| 21:9 | 超宽 | 电影感、横幅、全景 |
| 1:4 | 超竖 | 手机长图、漫画 |
| 4:1 | 超横 | 横幅、网站头图 |
| 1:8 | 超竖 | 手机长图、漫画 |
| 8:1 | 超横 | 横幅、网站头图 |
resolution - 图片分辨率
1K、2K、4K三种分辨率选项 注意: 分辨率值必须大写(1K、2K、4K) 默认: 2K
注意事项
- API密钥必须设置,可通过环境变量或命令行参数提供
- 分辨率参数必须大写(1K/2K/4K),小写会默认使用1K
- 图片生成时间:25秒到5分钟不等,取决于分辨率和服务器负载
- 编辑图片时,输入图片会自动转换为base64编码
- 确保输出目录有写入权限
API Key设置与获取
如何获取API Key
如果你还没有API密钥,请前往 https://api.apiyi.com 注册账号并申请API Key。
获取步骤: 1. 访问 https://api.apiyi.com 2. 注册/登录你的账号 3. 在控制台中创建API密钥 4. 复制密钥并设置环境变量或在命令行中使用
设置API Key
脚本按以下顺序查找API密钥: 1. --api-key 命令行参数(临时使用) 2. APIYI_API_KEY 环境变量(推荐)
设置环境变量(推荐):
# Linux/Mac
export APIYI_API_KEY="your-api-key-here"
# Windows CMD
我的电脑高级设置中设置环境变量或者执行set APIYI_API_KEY=your-api-key-here
# Windows PowerShell
在我的电脑中设置环境变量:$env:APIYI_API_KEY="your-api-key-here"命令行参数方式(临时):
python scripts/generate_image.py -p "一只猫" -k "your-api-key-here"作者介绍
- 爱海贼的无处不在
- 我的微信公众号:无处不在的技术
常见使用场景
社交媒体内容
- 微信朋友圈/公众号:
-a 1:1或-a 3:4, 或-a 9:16,配图文案图、封面图 - 小红书笔记:
-a 3:4或-a 4:5,竖版配图更受欢迎 - 抖音/视频号:
-a 9:16,短视频封面、竖版内容图 - B站/知乎:
-a 16:9 -r 2K,视频封面、专栏头图 - 微博:
-a 1:1或-a 3:4,九宫格配图、话题图
电商与营销
- 淘宝/京东主图:
-a 1:1,白底产品图、场景图 - 电商详情页:
-a 3:4,产品展示、卖点图、对比图 - 活动海报:
-a 2:3或-a 9:16,促销海报、节日海报 - PPT配图:
-a 16:9,演讲背景、数据可视化插图
个人创作
- 艺术创作: 视觉信息图、笔记图、插画图等等
- 头像/壁纸:
-a 1:1(头像)、-a 9:16(手机壁纸) - 表情包/梗图:
-a 1:1,搞笑图片、表情包素材 - 节日贺卡:
-a 3:4或-a 4:5,春节、中秋祝福图 - LOGO/图标:
-a 1:1,品牌标识、应用图标设计
编辑任务
- 风格转换: "转换成国潮/水墨/二次元/像素风格"
- 元素添加: "添加春节元素/红包/灯笼/烟花"
- 背景替换: "将背景换成故宫/西湖/长城/现代都市"
- 色彩调整: "调整为莫兰迪色系/中国传统色"
- 对象移除: "去除水印/路人/杂物"
- 画质修复: "修复老照片/提升清晰度/去噪点"
#!/usr/bin/env node
/*
基于NanoBanana2的图片生成与编辑脚本(Node.js版)
使用API易国内代理服务
支持功能:
- 文生图:根据提示词生成图片
- 图生图:根据编辑指令修改已有图片
参数说明:
- -p, --prompt 图片描述或编辑指令文本(必需)
- -f, --filename 输出图片路径(可选,默认自动生成时间戳文件名)
- -a, --aspect-ratio 图片比例(可选)
- -r, --resolution 图片分辨率(可选:1K/2K/4K,必须大写)
- -i, --input-image 输入图片路径(可选,可多张,最多14张)
- -k, --api-key API密钥(可选,覆盖环境变量 APIYI_API_KEY)
使用示例:
【生成新图片】
node generate_image.js -p "一只可爱的橘猫"
node generate_image.js -p "日落山脉" -a 16:9 -r 4K
node generate_image.js -p "城市夜景" -a 9:16 -r 2K -f wallpaper.png
【编辑已有图片】
node generate_image.js -p "转换成油画风格" -i original.png
node generate_image.js -p "添加彩虹到天空" -i photo.jpg -f edited.png
node generate_image.js -p "将背景换成海滩" -i portrait.png -a 3:4 -r 2K
node generate_image.js -p "参考多张图片融合风格" -i ref1.png ref2.png ref3.png -f merged.png
【环境变量】
export APIYI_API_KEY="your-api-key"
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const SUPPORTED_ASPECT_RATIOS = [
'1:1',
'16:9',
'9:16',
'4:3',
'3:4',
'3:2',
'2:3',
'5:4',
'4:5',
'1:4',
'4:1',
'1:8',
'8:1',
'21:9',
];
const SUPPORTED_RESOLUTIONS = ['1K', '2K', '4K'];
function printHelpAndExit(exitCode = 0) {
const help = `usage: generate_image.js [-h] --prompt PROMPT [--filename FILENAME]
[--aspect-ratio ${SUPPORTED_ASPECT_RATIOS.join(', ')}]
[--resolution ${SUPPORTED_RESOLUTIONS.join(', ')}]
[--input-image INPUT_IMAGE [INPUT_IMAGE ...]]
[--api-key API_KEY]
基于NanoBanana2的图片生成与编辑工具(Node.js版)
options:
-h, --help show this help message and exit
-p, --prompt PROMPT 图片描述或编辑指令文本(必需)
-f, --filename FILE 输出图片路径 (默认: 自动生成时间戳文件名)
-a, --aspect-ratio 图片比例 (可选)
-r, --resolution 图片分辨率 (可选: 1K, 2K, 4K,必须大写)
-i, --input-image 输入图片路径(编辑模式,可传多张,最多14张)
-k, --api-key API密钥(覆盖环境变量)
运行示例:
node scripts/generate_image.js -p "一只可爱的橘猫"
node scripts/generate_image.js -p "日落山脉" -a 16:9 -r 4K
node scripts/generate_image.js -p "城市夜景" -a 9:16 -r 2K -f wallpaper.png
node scripts/generate_image.js -p "转换成油画风格" -i original.png
node scripts/generate_image.js -p "参考多张图片融合风格" -i ref1.png ref2.png -f merged.png
`;
process.stdout.write(help);
process.exit(exitCode);
}
function exitWithError(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}
function pad2(n) {
return String(n).padStart(2, '0');
}
function formatTimestamp(dateObj) {
const d = dateObj || new Date();
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}-${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
}
function addTimestampToFilename(filePath, timestamp) {
const ts = timestamp || formatTimestamp(new Date());
const parsed = path.parse(filePath);
const base = parsed.name ? `${parsed.name}-${ts}` : ts;
return path.join(parsed.dir || '.', `${base}${parsed.ext || ''}`);
}
function generateFilename(prompt) {
const now = new Date();
const timestamp = formatTimestamp(now);
const keywords = String(prompt).split(/\s+/).filter(Boolean).slice(0, 3);
const keywordStrRaw = keywords.join('-') || 'image';
const keywordStr = keywordStrRaw
.split('')
.map((c) => (/^[a-zA-Z0-9\-_.]$/.test(c) ? c : '-'))
.join('')
.toLowerCase()
.slice(0, 30);
return `${timestamp}-${keywordStr}.png`;
}
function getApiKey(argsKey) {
if (argsKey) return argsKey;
const apiKey = process.env.APIYI_API_KEY;
if (!apiKey) {
exitWithError(
'错误: 未设置 APIYI_API_KEY 环境变量\n' +
'请前往 https://api.apiyi.com 注册申请API Key\n' +
'或使用 -k/--api-key 参数临时指定'
);
}
return apiKey;
}
function encodeImageToBase64(imagePath) {
try {
const bytes = fs.readFileSync(imagePath);
return bytes.toString('base64');
} catch (e) {
exitWithError(`错误: 无法读取图片文件 ${imagePath} - ${e.message || String(e)}`);
}
}
function postJson(urlString, headers, payload, timeoutMs) {
return new Promise((resolve, reject) => {
const url = new URL(urlString);
const body = Buffer.from(JSON.stringify(payload), 'utf8');
const req = https.request(
{
protocol: url.protocol,
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: 'POST',
headers: {
...headers,
'Content-Length': body.length,
},
},
(res) => {
const chunks = [];
res.on('data', (d) => chunks.push(d));
res.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8');
const statusCode = res.statusCode || 0;
if (statusCode < 200 || statusCode >= 300) {
const err = new Error(`HTTP ${statusCode}`);
err.statusCode = statusCode;
err.responseText = text;
return reject(err);
}
try {
resolve(JSON.parse(text));
} catch (e) {
const err = new Error('响应不是有效的JSON');
err.responseText = text;
return reject(err);
}
});
}
);
req.on('error', reject);
req.setTimeout(timeoutMs, () => {
req.destroy(new Error('timeout'));
});
req.write(body);
req.end();
});
}
function parseArgs(argv) {
const args = {
prompt: null,
filename: null,
aspectRatio: null,
resolution: null,
inputImages: null,
apiKey: null,
};
const knownFlags = new Set([
'-h',
'--help',
'-p',
'--prompt',
'-f',
'--filename',
'-a',
'--aspect-ratio',
'-r',
'--resolution',
'-i',
'--input-image',
'-k',
'--api-key',
]);
function requireValue(i, flag) {
const v = argv[i + 1];
if (!v || (v.startsWith('-') && knownFlags.has(v))) {
exitWithError(`错误: 参数 ${flag} 需要一个值`);
}
return v;
}
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '-h' || a === '--help') {
printHelpAndExit(0);
}
if (a === '-p' || a === '--prompt') {
args.prompt = requireValue(i, a);
i++;
continue;
}
if (a === '-f' || a === '--filename') {
args.filename = requireValue(i, a);
i++;
continue;
}
if (a === '-a' || a === '--aspect-ratio') {
args.aspectRatio = requireValue(i, a);
i++;
continue;
}
if (a === '-r' || a === '--resolution') {
args.resolution = requireValue(i, a);
i++;
continue;
}
if (a === '-k' || a === '--api-key') {
args.apiKey = requireValue(i, a);
i++;
continue;
}
if (a === '-i' || a === '--input-image') {
const images = [];
let j = i + 1;
while (j < argv.length) {
const v = argv[j];
if (v.startsWith('-') && knownFlags.has(v)) break;
images.push(v);
j++;
}
if (images.length === 0) {
exitWithError(`错误: 参数 ${a} 需要至少一个图片路径`);
}
args.inputImages = images;
i = j - 1;
continue;
}
if (a.startsWith('-')) {
exitWithError(`错误: 未知参数 ${a},请使用 --help 查看帮助`);
}
}
if (!args.prompt) {
exitWithError('错误: 缺少必需参数 -p/--prompt');
}
return args;
}
async function main() {
const argv = process.argv.slice(2);
const args = parseArgs(argv);
const runTimestamp = formatTimestamp(new Date());
let checkProgress = null;
const clearProgressTimer = () => {
if (checkProgress) {
clearInterval(checkProgress);
checkProgress = null;
}
};
if (args.aspectRatio != null && !SUPPORTED_ASPECT_RATIOS.includes(args.aspectRatio)) {
exitWithError(
`错误: 不支持的比例 '${args.aspectRatio}'\n支持的比例: ${SUPPORTED_ASPECT_RATIOS.join(', ')}`
);
}
if (args.resolution != null && !SUPPORTED_RESOLUTIONS.includes(args.resolution)) {
exitWithError(
`错误: 不支持的分辨率 '${args.resolution}'\n支持的分辨率: ${SUPPORTED_RESOLUTIONS.join(', ')} (必须大写)`
);
}
if (!args.filename) {
args.filename = generateFilename(args.prompt);
} else {
const resolved = path.resolve(args.filename);
if (fs.existsSync(resolved)) {
const adjusted = addTimestampToFilename(args.filename, runTimestamp);
process.stdout.write(`⚠️ 输出文件已存在,将避免覆盖并改为: ${adjusted}\n`);
args.filename = adjusted;
}
}
const apiKey = getApiKey(args.apiKey);
const url =
'https://api.apiyi.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent';
const headers = {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
};
const parts = [{ text: args.prompt }];
let modeStr = '生成图片';
if (args.inputImages && args.inputImages.length > 0) {
if (args.inputImages.length > 14) {
exitWithError(`错误: 输入图片最多支持14张,当前为 ${args.inputImages.length} 张`);
}
for (const imgPath of args.inputImages) {
if (!fs.existsSync(imgPath)) {
exitWithError(`错误: 输入图片不存在: ${imgPath}`);
}
const imageBase64 = encodeImageToBase64(imgPath);
parts.push({
inlineData: {
mimeType: 'image/png',
data: imageBase64,
},
});
}
modeStr = '编辑图片';
}
const generationConfig = {
responseModalities: ['IMAGE'],
};
const imageConfig = {};
if (args.aspectRatio != null) imageConfig.aspectRatio = args.aspectRatio;
if (args.resolution != null) imageConfig.imageSize = args.resolution;
if (Object.keys(imageConfig).length > 0) generationConfig.imageConfig = imageConfig;
const payload = {
contents: [{ parts }],
generationConfig,
};
// 生成前通知 + 生成中实时日志(避免长时间无输出导致体验不佳)
const resolutionHint = args.resolution;
const etaText = resolutionHint === '4K' ? '1-6分钟' : '30-120秒';
process.stdout.write('🎨 图片生成已启动!\n');
process.stdout.write(`⏱️ 预计时间: ${etaText}\n`);
process.stdout.write('📊 我会定期给您发送进度更新\n');
process.stdout.write(`正在${modeStr}...\n`);
process.stdout.write(`提示词: ${args.prompt}\n`);
if (generationConfig.imageConfig && generationConfig.imageConfig.aspectRatio) {
process.stdout.write(`比例: ${generationConfig.imageConfig.aspectRatio}\n`);
}
if (generationConfig.imageConfig && generationConfig.imageConfig.imageSize) {
process.stdout.write(`分辨率: ${generationConfig.imageConfig.imageSize}\n`);
}
// 输出请求参数(脱敏:不直接输出base64图片数据,避免刷屏)
const payloadLog = {
generationConfig,
contents: [],
};
for (const content of payload.contents || []) {
const partsLog = [];
for (const part of content.parts || []) {
if (part && typeof part === 'object' && part.inlineData && typeof part.inlineData === 'object') {
const inlineData = { ...part.inlineData };
if (typeof inlineData.data === 'string') {
inlineData.data = `<omitted base64: ${inlineData.data.length} chars>`;
}
partsLog.push({ inlineData });
} else {
partsLog.push(part);
}
}
payloadLog.contents.push({ parts: partsLog });
}
process.stdout.write(`输出请求参数: ${JSON.stringify(payloadLog, null, 2)}\n`);
process.stdout.write('image generation in progress...\n');
const startTime = Date.now();
checkProgress = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
process.stdout.write(`🔄 已进行 ${elapsed}秒...\n`);
}, 5000);
let data;
try {
data = await postJson(url, headers, payload, 120_000);
} catch (e) {
clearProgressTimer();
if (e && e.message === 'timeout') {
exitWithError('错误: 请求超时,请稍后重试');
}
if (e && e.statusCode) {
process.stderr.write(`错误: 请求失败 - HTTP ${e.statusCode}\n`);
if (e.responseText) {
try {
const detail = JSON.parse(e.responseText);
process.stderr.write(`错误详情: ${JSON.stringify(detail, null, 2)}\n`);
} catch {
process.stderr.write(`响应内容: ${e.responseText}\n`);
}
}
process.exit(1);
}
exitWithError(`错误: 请求失败 - ${e.message || String(e)}`);
}
clearProgressTimer();
const imageData =
data &&
data.candidates &&
Array.isArray(data.candidates) &&
data.candidates[0] &&
data.candidates[0].content &&
data.candidates[0].content.parts &&
data.candidates[0].content.parts[0] &&
data.candidates[0].content.parts[0].inlineData &&
data.candidates[0].content.parts[0].inlineData.data;
if (!imageData) {
process.stderr.write('错误: 响应中未找到图片数据\n');
process.stderr.write(`完整响应: ${JSON.stringify(data, null, 2)}\n`);
process.exit(1);
}
const imageBytes = Buffer.from(imageData, 'base64');
const outputFile = path.resolve(args.filename);
const outputDir = path.dirname(outputFile);
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(outputFile, imageBytes);
process.stdout.write(`✓ 图片已成功${modeStr}并保存到: ${args.filename}\n`);
process.stdout.write('✅ 生成完成!\n');
}
main().catch((e) => {
exitWithError(`错误: ${e && e.message ? e.message : String(e)}`);
});
#!/usr/bin/env python3
"""
基于NanoBanana2的图片生成与编辑脚本
使用API易国内代理服务
支持功能:
- 文生图:根据提示词生成图片
- 图生图:根据编辑指令修改已有图片
参数说明:
- aspect_ratio: 图片比例 (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 1:4, 4:1, 1:8, 8:1, 21:9)
- resolution: 图片分辨率 (1K, 2K, 4K),必须大写
"""
import os
import sys
import json
import base64
import argparse
from unittest import result
import requests
from pathlib import Path
from datetime import datetime
# 支持的比例列表
SUPPORTED_ASPECT_RATIOS = [
"1:1",
"16:9",
"9:16",
"4:3",
"3:4",
"3:2",
"2:3",
"5:4",
"4:5",
"1:4",
"4:1",
"1:8",
"8:1",
"21:9",
]
SUPPORTED_RESOLUTIONS = ["1K", "2K", "4K"]
def get_api_key(args_key=None):
"""获取API密钥,优先使用命令行参数"""
if args_key:
return args_key
api_key = os.environ.get("APIYI_API_KEY")
if not api_key:
print("错误: 未设置 APIYI_API_KEY 环境变量")
print("请前往 https://api.apiyi.com 注册申请API Key")
print("或使用 --api-key 参数临时指定")
sys.exit(1)
return api_key
def generate_filename(prompt):
"""根据提示词生成带时间戳的文件名"""
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
# 从提示词提取关键词(简化处理)
keywords = prompt.split()[:3] # 取前3个词
keyword_str = "-".join(keywords)
# 清理文件名中的特殊字符
keyword_str = "".join(c if c.isalnum() or c in "-_." else "-" for c in keyword_str)
keyword_str = keyword_str.lower()[:30] # 限制长度
return f"{timestamp}-{keyword_str}.png"
def add_timestamp_to_filename(filename: str, timestamp: str) -> str:
p = Path(filename)
stem = p.stem or "image"
suffix = p.suffix
# If suffix is empty, keep it empty (caller may intentionally omit extension)
new_name = f"{stem}-{timestamp}{suffix}"
return str(p.with_name(new_name))
def encode_image_to_base64(image_path):
"""将图片文件转换为base64编码"""
try:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
except Exception as e:
print(f"错误: 无法读取图片文件 {image_path} - {e}")
sys.exit(1)
def generate_image(
prompt,
filename,
aspect_ratio=None,
resolution=None,
input_image=None,
api_key=None,
):
"""
生成或编辑图片
Args:
prompt: 图片描述或编辑指令文本
filename: 输出图片路径
aspect_ratio: 图片比例 (可选,默认由API决定)
resolution: 图片分辨率 (可选,默认由API决定)
input_image: 输入图片路径(编辑模式时使用)
api_key: API密钥
"""
# 验证参数(仅在提供了参数时验证)
if aspect_ratio is not None and aspect_ratio not in SUPPORTED_ASPECT_RATIOS:
print(f"错误: 不支持的比例 '{aspect_ratio}'")
print(f"支持的比例: {', '.join(SUPPORTED_ASPECT_RATIOS)}")
sys.exit(1)
if resolution is not None and resolution not in SUPPORTED_RESOLUTIONS:
print(f"错误: 不支持的分辨率 '{resolution}'")
print(f"支持的分辨率: {', '.join(SUPPORTED_RESOLUTIONS)} (必须大写)")
sys.exit(1)
api_key = get_api_key(api_key)
url = (
"https://api.apiyi.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent"
)
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
# 构建content部分
parts = [{"text": prompt}]
# 如果提供了输入图片,添加图片数据(图生图模式)
# NanoBanana2最多支持14张参考图片
if input_image:
if isinstance(input_image, (list, tuple)):
input_images = list(input_image)
else:
input_images = [input_image]
if len(input_images) > 14:
print(f"错误: 输入图片最多支持14张,当前为 {len(input_images)} 张")
sys.exit(1)
for image_path in input_images:
if not os.path.exists(image_path):
print(f"错误: 输入图片不存在: {image_path}")
sys.exit(1)
image_base64 = encode_image_to_base64(image_path)
parts.append({"inlineData": {"mimeType": "image/png", "data": image_base64}})
mode_str = "编辑图片"
else:
mode_str = "生成图片"
# 构建payload,只添加用户指定的参数
generation_config = {
"responseModalities": ["IMAGE"],
}
image_config = {}
if aspect_ratio is not None:
image_config["aspectRatio"] = aspect_ratio
if resolution is not None:
image_config["imageSize"] = resolution
if image_config:
generation_config["imageConfig"] = image_config
payload = {
"contents": [{"parts": parts}],
"generationConfig": generation_config,
}
print(f"正在{mode_str}...")
print(f"提示词: {prompt}")
if generation_config.get("imageConfig", {}).get("aspectRatio"):
print(f"比例: {generation_config['imageConfig']['aspectRatio']}")
if generation_config.get("imageConfig", {}).get("imageSize"):
print(f"分辨率: {generation_config['imageConfig']['imageSize']}")
# 输出请求参数(脱敏:不直接输出base64图片数据,避免刷屏)
payload_log = {
"generationConfig": generation_config,
"contents": [],
}
for content in payload.get("contents", []):
parts_log = []
for part in content.get("parts", []):
if isinstance(part, dict) and "inlineData" in part and isinstance(part["inlineData"], dict):
inline_data = dict(part["inlineData"])
data_value = inline_data.get("data")
if isinstance(data_value, str):
inline_data["data"] = f"<omitted base64: {len(data_value)} chars>"
parts_log.append({"inlineData": inline_data})
else:
parts_log.append(part)
payload_log["contents"].append({"parts": parts_log})
print(f"输出请求参数: {json.dumps(payload_log, indent=2, ensure_ascii=False)}")
print(f"image generation in progress...")
try:
response = requests.post(url, headers=headers, json=payload, timeout=400)
response.raise_for_status()
data = response.json()
# 解析响应,查找图片数据
image_data = None
text_response = ""
if "candidates" in data and len(data["candidates"]) > 0:
candidate = data["candidates"][0]
image_data = candidate["content"]["parts"][0]["inlineData"]["data"]
if image_data:
# 解码base64图片数据
image_bytes = base64.b64decode(image_data)
# 确保输出目录存在
output_file = Path(filename)
output_file.parent.mkdir(parents=True, exist_ok=True)
# 保存图片
with open(output_file, "wb") as f:
f.write(image_bytes)
print(f"✓ 图片已成功{mode_str}并保存到: {filename}")
if text_response.strip():
print(f"模型响应: {text_response.strip()}")
return filename
else:
print("错误: 响应中未找到图片数据")
print(f"完整响应: {json.dumps(data, indent=2, ensure_ascii=False)}")
sys.exit(1)
except requests.exceptions.Timeout:
print("错误: 请求超时,请稍后重试")
sys.exit(1)
except requests.exceptions.RequestException as e:
print(f"错误: 请求失败 - {e}")
if hasattr(e, "response") and e.response is not None:
try:
error_detail = e.response.json()
print(
f"错误详情: {json.dumps(error_detail, indent=2, ensure_ascii=False)}"
)
except:
print(f"响应状态码: {e.response.status_code}")
print(f"响应内容: {e.response.text}")
sys.exit(1)
except Exception as e:
print(f"错误: {str(e)}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="基于Gemini 3 Pro的图片生成与编辑工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
【生成新图片】
python generate_image.py -p "一只可爱的橘猫"
python generate_image.py -p "日落山脉" -a 16:9 -r 4K
python generate_image.py -p "城市夜景" -a 9:16 -r 2K -f wallpaper.png
【编辑已有图片】
python generate_image.py -p "转换成油画风格" -i original.png
python generate_image.py -p "添加彩虹到天空" -i photo.jpg -f edited.png
python generate_image.py -p "将背景换成海滩" -i portrait.png -a 3:4 -r 2K
python generate_image.py -p "参考多张图片融合风格" -i ref1.png ref2.png ref3.png -f merged.png
【支持的参数值】
--aspect-ratio: 可选 (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 1:4, 4:1, 1:8, 8:1, 21:9)
--resolution: 可选 (1K, 2K, 4K,必须大写)
【环境变量】
export APIYI_API_KEY="your-api-key"
""",
)
parser.add_argument("--prompt", "-p", required=True, help="图片描述或编辑指令文本")
parser.add_argument(
"--filename",
"-f",
default=None,
help="输出图片路径 (默认: 自动生成时间戳文件名)",
)
parser.add_argument(
"--aspect-ratio",
"-a",
default=None,
choices=SUPPORTED_ASPECT_RATIOS,
help="图片比例 (可选: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 1:4, 4:1, 1:8, 8:1, 21:9)",
)
parser.add_argument(
"--resolution",
"-r",
default=None,
choices=SUPPORTED_RESOLUTIONS,
help="图片分辨率 (可选: 1K, 2K, 4K,必须大写)",
)
parser.add_argument(
"--input-image",
"-i",
nargs="+",
default=None,
help="输入图片路径(编辑模式,可传多张,最多14张)",
)
parser.add_argument("--api-key", "-k", default=None, help="API密钥(覆盖环境变量)")
args = parser.parse_args()
run_timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
# 如果没有指定文件名,自动生成
if args.filename is None:
args.filename = generate_filename(args.prompt)
else:
out_path = Path(args.filename)
if out_path.exists():
adjusted = add_timestamp_to_filename(args.filename, run_timestamp)
print(f"警告: 输出文件已存在,将避免覆盖并改为: {adjusted}")
args.filename = adjusted
generate_image(
prompt=args.prompt,
filename=args.filename,
aspect_ratio=args.aspect_ratio,
resolution=args.resolution,
input_image=args.input_image,
api_key=args.api_key,
)
if __name__ == "__main__":
main()