AI 图像生成功能
基于火山引擎豆包 API 的图生图实现指南
AI 图像生成功能
Sistine Starter 集成了火山引擎豆包 API 的图生图 (Image-to-Image) 功能,支持基于参考图像进行 AI 图像编辑和生成。本文档详细介绍图像生成系统的架构、实现细节和集成方法。
功能概览
核心特性
- ✅ 图生图 (I2I): 基于参考图像进行编辑和生成
- ✅ 积分扣费: 每次生成消耗 20 积分,事务性扣费确保数据一致性
- ✅ 结果存储: 生成结果自动上传到 R2 存储,长期保存
- ✅ 生成历史: 完整记录每次生成的参数和结果
- ✅ 多尺寸支持: 支持 adaptive / 1K / 2K / 4K 分辨率
- ✅ 水印控制: 可选择是否添加水印
技术架构
用户上传图像 + 输入提示词
↓
身份认证 (Better Auth)
↓
检查积分余额 (20 积分)
↓
创建生成记录 (generationHistory)
↓
扣除积分 + 记录账本
↓
调用火山引擎图像 API
↓
上传结果到 R2 存储
↓
更新生成记录 (completed)
↓
返回结果 URL
火山引擎配置
模型选择
当前使用的模型是 doubao-seededit-3-0-i2i-250628
,这是火山引擎豆包系列的图生图专用模型。
模型特性:
- 支持基于参考图像的编辑和生成
- 高质量输出,细节保留良好
- 支持多种分辨率 (最高 4K)
- 生成速度: 约 5-10 秒/张
配置位置: lib/volcano-engine/image.ts:20
const model = volcanoEngineConfig.imageModel || 'doubao-seededit-3-0-i2i-250628';
API 参数配置
可用参数:
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
prompt | string | 必需 | 图像编辑的提示词描述 |
image | string[] | 必需 | 参考图像 URL 数组 (图生图模式) |
size | string | adaptive | 输出分辨率 (adaptive / 1K / 2K / 4K ) |
watermark | boolean | true | 是否添加水印 |
response_format | string | url | 返回格式 (固定为 url ) |
调整参数示例:
// lib/volcano-engine/image.ts
const request: ImageGenerationRequest = {
model,
prompt,
image: [referenceImageUrl], // 参考图像
response_format: 'url',
size: '2K', // 使用 2K 分辨率
watermark: false, // 不添加水印
};
分辨率说明
选项 | 说明 | 适用场景 |
---|---|---|
adaptive | 自适应 (根据输入图像自动选择) | 通用场景,推荐默认值 |
1K | 约 1024x1024 | 快速预览,节省积分 |
2K | 约 2048x2048 | 高质量输出 |
4K | 约 4096x4096 | 超高清输出 (消耗更多时间) |
数据库架构
生成历史表 (generationHistory)
存储所有 AI 生成操作的记录 (包括图像和视频)。建表语句请参考 数据库
文档中的 generationHistory
表定义。
关键字段说明:
字段 | 类型 | 说明 |
---|---|---|
type | TEXT | 'image' (图像生成) 或 'video' (视频生成) |
prompt | TEXT | 用户输入的提示词描述 |
imageUrl | TEXT | 参考图像 URL (图生图模式必需) |
resultUrl | TEXT | 生成完成后的结果 URL (存储在 R2) |
status | TEXT | 任务状态: processing → completed / failed |
creditsUsed | INTEGER | 记录消耗的积分 (20) |
metadata | TEXT | JSON 字符串,存储生成参数 (size, watermark 等) |
Metadata 示例:
{
"size": "2K",
"watermark": false,
"imageUrl": "https://example.com/reference.jpg"
}
API 实现
API 端点
路由: POST /api/image/generate
实现文件: app/api/image/generate/route.ts
请求格式
// POST /api/image/generate
{
"prompt": "将这张照片转换为水彩画风格",
"imageUrl": "https://example.com/reference.jpg",
"size": "2K", // 可选, 默认 'adaptive'
"watermark": false // 可选, 默认 true
}
参数说明:
参数 | 类型 | 必需 | 说明 |
---|---|---|---|
prompt | string | 是 | 图像编辑的描述 (如 "转换为水彩画风格") |
imageUrl | string | 是 | 参考图像的 URL (必须是可访问的公开 URL) |
size | string | 否 | 输出分辨率 (adaptive / 1K / 2K / 4K ) |
watermark | boolean | 否 | 是否添加水印,默认 true |
响应格式
成功响应 (200)
{
"id": "uuid-xxx-xxx",
"url": "https://your-r2-storage.com/images/user_123/image_xxx.png",
"revisedPrompt": "将这张照片转换为水彩画风格",
"remainingCredits": 280,
"sourceImageUrl": "https://example.com/reference.jpg"
}
字段说明:
字段 | 类型 | 说明 |
---|---|---|
id | string | 生成记录 ID,可用于查询历史 |
url | string | 生成图像的 URL (存储在 R2) |
revisedPrompt | string | API 优化后的提示词 (可能与输入略有不同) |
remainingCredits | number | 扣费后用户剩余积分 |
sourceImageUrl | string | 输入的参考图像 URL |
错误响应
// 401 - 未登录
{
"error": "Unauthorized"
}
// 400 - 缺少必需参数
{
"error": "Prompt is required"
}
// 400 - 缺少参考图像
{
"error": "Reference image is required"
}
// 402 - 积分不足
{
"error": "Insufficient credits",
"creditsNeeded": 20,
"remainingCredits": 5
}
// 500 - 生成失败
{
"error": "Failed to generate image"
}
核心实现逻辑
步骤 1: 身份认证与权限检查
// 1. 验证用户登录状态
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.session?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.session.userId;
步骤 2: 解析请求并验证
// 2. 解析请求体
const { prompt, size, watermark, imageUrl } = await req.json();
// 3. 验证必需参数
if (!prompt) {
return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
}
if (!imageUrl) {
return NextResponse.json({ error: "Reference image is required" }, { status: 400 });
}
重要: 图生图模式必须提供参考图像,否则无法生成。
步骤 3: 检查积分余额
// 4. 检查用户是否有足够积分 (20 积分)
const creditsNeeded = 20;
const hasCredits = await canUserAfford(userId, creditsNeeded);
if (!hasCredits) {
return NextResponse.json({
error: "Insufficient credits",
creditsNeeded,
remainingCredits: 0
}, { status: 402 });
}
步骤 4: 创建生成记录
// 5. 创建生成历史记录
const historyId = randomUUID();
await db.insert(generationHistory).values({
id: historyId,
userId,
type: "image",
prompt,
status: "processing",
creditsUsed: creditsNeeded,
metadata: JSON.stringify({ size, watermark, imageUrl }),
});
为什么先创建记录?
- 即使后续步骤失败,也能追踪尝试记录
- 提供关联 ID 用于积分账本记录
- 便于管理员排查问题
步骤 5: 扣除积分
// 6. 扣除积分 (事务性操作)
const deductResult = await deductCredits(
userId,
creditsNeeded,
"image_generation",
historyId
);
if (!deductResult.success) {
// 更新记录为失败状态
await db.update(generationHistory)
.set({ status: "failed", error: deductResult.error })
.where(eq(generationHistory.id, historyId));
return NextResponse.json({
error: deductResult.error || "Failed to deduct credits",
remainingCredits: deductResult.remainingCredits
}, { status: 402 });
}
步骤 6: 调用火山引擎 API
try {
// 7. 生成图像
const result = await volcanoEngine.generateImage(prompt, {
size,
inputImages: [imageUrl], // 传入参考图像
watermark,
});
if (!result.data || result.data.length === 0) {
throw new Error('No image generated');
}
const imageData = result.data[0];
// imageData.url: 火山引擎返回的图像 URL
// imageData.revised_prompt: 优化后的提示词
步骤 7: 上传到 R2 存储
// 8. 上传图像到 R2 存储 (长期保存)
const r2Url = await uploadImageFromUrl(imageData.url, userId, 'image');
为什么要上传到 R2?
- 火山引擎返回的 URL 有时效性 (通常 24 小时)
- R2 存储提供长期、稳定的访问
- 降低依赖外部服务的风险
步骤 8: 更新生成记录
// 9. 更新记录为完成状态
await db.update(generationHistory)
.set({
status: "completed",
resultUrl: r2Url,
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
// 10. 返回结果
return NextResponse.json({
id: historyId,
url: r2Url,
revisedPrompt: imageData.revised_prompt,
remainingCredits: deductResult.remainingCredits,
sourceImageUrl: imageUrl,
});
} catch (genError: any) {
// 11. 失败处理: 更新记录状态
await db.update(generationHistory)
.set({
status: "failed",
error: genError.message,
updatedAt: new Date(),
})
.where(eq(generationHistory.id, historyId));
throw genError;
}
注意: 当前实现不会自动退还积分,即使生成失败。如果需要退款机制,可以添加:
catch (genError: any) {
// 退还积分
await refundCredits(userId, creditsNeeded, "refund", historyId);
// 更新记录...
}
积分扣费机制
扣费规则
- 消耗标准: 每次图像生成消耗 20 积分
- 扣费时机: 在调用 API 之前扣除,防止积分不足时浪费 API 调用
- 失败退款: 当前不会自动退款 (可根据需求自行添加)
积分消耗定义
定义位置: app/api/image/generate/route.ts:36
const creditsNeeded = 20; // 图像生成消耗 20 积分
调整消耗量:
// 根据分辨率动态定价
const creditsCost = {
'adaptive': 20,
'1K': 15,
'2K': 25,
'4K': 40,
};
const creditsNeeded = creditsCost[size] || 20;
客户端集成示例
前端实现 (React)
import { useState } from "react";
export function ImageGeneratorComponent() {
const [prompt, setPrompt] = useState("");
const [imageUrl, setImageUrl] = useState("");
const [resultUrl, setResultUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [credits, setCredits] = useState<number>(0);
const handleGenerate = async () => {
if (!prompt || !imageUrl) {
alert("请输入提示词和参考图像 URL");
return;
}
setIsLoading(true);
setResultUrl(null);
try {
const response = await fetch("/api/image/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
imageUrl,
size: "2K",
watermark: false,
}),
});
if (!response.ok) {
const error = await response.json();
if (response.status === 402) {
alert(`积分不足!需要 ${error.creditsNeeded} 积分,当前余额 ${error.remainingCredits}`);
return;
}
throw new Error(error.error || "Failed to generate image");
}
const result = await response.json();
setResultUrl(result.url);
setCredits(result.remainingCredits);
console.log("生成记录 ID:", result.id);
console.log("优化后的提示词:", result.revisedPrompt);
} catch (error: any) {
console.error("Image generation error:", error);
alert(error.message);
} finally {
setIsLoading(false);
}
};
return (
<div>
{/* 积分显示 */}
<div>剩余积分: {credits}</div>
{/* 输入区域 */}
<div>
<label>参考图像 URL:</label>
<input
type="text"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://example.com/image.jpg"
disabled={isLoading}
/>
</div>
<div>
<label>提示词:</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="将这张照片转换为水彩画风格"
disabled={isLoading}
/>
</div>
<button onClick={handleGenerate} disabled={isLoading || !prompt || !imageUrl}>
{isLoading ? "生成中..." : "生成图像 (20 积分)"}
</button>
{/* 结果显示 */}
{resultUrl && (
<div>
<h3>生成结果:</h3>
<img src={resultUrl} alt="Generated" style={{ maxWidth: "100%" }} />
<a href={resultUrl} download>下载图像</a>
</div>
)}
</div>
);
}
带文件上传的实现
如果需要用户上传本地图像,需要先上传到服务器获取 URL:
const handleFileUpload = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const { url } = await response.json();
setImageUrl(url);
};
Fetch API 示例 (纯 JavaScript)
async function generateImage(prompt, imageUrl, options = {}) {
const response = await fetch('/api/image/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
imageUrl,
size: options.size || '2K',
watermark: options.watermark !== undefined ? options.watermark : false,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
return response.json();
}
// 使用示例
generateImage(
'将这张照片转换为水彩画风格',
'https://example.com/photo.jpg',
{ size: '2K', watermark: false }
)
.then(result => {
console.log('生成成功!');
console.log('结果 URL:', result.url);
console.log('剩余积分:', result.remainingCredits);
})
.catch(error => console.error('生成失败:', error.message));
常见问题处理
1. 参考图像格式要求
支持的格式: JPEG, PNG, WebP
建议:
- 图像大小: 不超过 10MB
- 分辨率: 建议在 512x512 到 2048x2048 之间
- 确保图像 URL 可公开访问 (不需要认证)
2. 提示词编写技巧
有效的提示词示例:
✅ "将这张照片转换为水彩画风格,保留人物特征"
✅ "在图像中添加夕阳光效,色调偏暖"
✅ "移除背景,替换为纯白色"
✅ "将照片风格改为赛博朋克,增强霓虹灯效果"
无效的提示词:
❌ "好看" (太模糊)
❌ "修改" (没有明确指示)
❌ "画一只猫" (应该使用文生图,而非图生图)
3. 生成失败的常见原因
错误信息 | 可能原因 | 解决方案 |
---|---|---|
"Reference image is required" | 未提供参考图像 URL | 确保请求中包含 imageUrl 字段 |
"No image generated" | API 返回空结果 | 检查提示词是否有效,稍后重试 |
"Failed to upload to R2" | R2 存储配置错误 | 检查 STORAGE_* 环境变量 |
"Insufficient credits" | 积分不足 | 提示用户购买积分 |
高级功能
批量生成
async function batchGenerateImages(prompts: string[], imageUrl: string) {
const results = [];
for (const prompt of prompts) {
const result = await fetch('/api/image/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, imageUrl }),
}).then(r => r.json());
results.push(result);
// 添加延迟避免频繁调用
await new Promise(resolve => setTimeout(resolve, 1000));
}
return results;
}
// 使用
const prompts = [
'转换为水彩画风格',
'转换为油画风格',
'转换为素描风格',
];
batchGenerateImages(prompts, 'https://example.com/photo.jpg')
.then(results => console.log('批量生成完成:', results));
查询生成历史
// API 端点 (需要自己实现)
// GET /api/image/history?limit=10
async function getImageHistory(limit = 10) {
const response = await fetch(`/api/image/history?limit=${limit}`);
return response.json();
}
// 返回格式
/*
[
{
id: "uuid-xxx",
prompt: "转换为水彩画风格",
imageUrl: "https://...",
resultUrl: "https://...",
status: "completed",
createdAt: "2025-10-15T10:30:00Z"
},
...
]
*/
动态定价
根据分辨率调整积分消耗:
// app/api/image/generate/route.ts
const { prompt, imageUrl, size = 'adaptive', watermark } = await req.json();
// 根据分辨率计算积分
const creditsCostMap = {
'adaptive': 20,
'1K': 15,
'2K': 25,
'4K': 40,
};
const creditsNeeded = creditsCostMap[size] || 20;
// 检查余额
const hasCredits = await canUserAfford(userId, creditsNeeded);
// ... 后续逻辑
性能优化建议
1. 图像 URL 预加载
前端先预加载图像,确保 URL 有效:
const preloadImage = (url: string): Promise<void> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = () => reject(new Error('Failed to load image'));
img.src = url;
});
};
// 在提交前验证
await preloadImage(imageUrl);
2. R2 上传并行化
如果生成多张图像,可以并行上传:
const uploadPromises = imageUrls.map(url =>
uploadImageFromUrl(url, userId, 'image')
);
const r2Urls = await Promise.all(uploadPromises);
3. 缓存火山引擎 Token
如果使用自定义认证,可以缓存 Token 减少请求:
let cachedToken: string | null = null;
let tokenExpiry: number = 0;
async function getVolcanoToken() {
if (cachedToken && Date.now() < tokenExpiry) {
return cachedToken;
}
// 获取新 Token
const response = await fetch('https://api.volcengine.com/auth/token', { ... });
const { token, expires_in } = await response.json();
cachedToken = token;
tokenExpiry = Date.now() + expires_in * 1000;
return token;
}
相关文档
- 积分系统 - 积分扣费和账本管理
- AI 对话功能 - 对话功能文档
- AI 视频生成 - 视频生成功能文档
- 火山引擎图像 API 文档 - 官方 API 文档
总结
AI 图像生成功能通过以下关键设计提供稳定的服务:
- 图生图模式: 基于参考图像进行编辑,保留原始特征
- 长期存储: 自动上传到 R2,避免临时 URL 失效
- 事务性扣费: 确保积分扣除和记录的一致性
- 完整历史: 记录每次生成的参数和结果,便于追溯
按照本文档的指导,你可以快速集成图像生成功能并根据业务需求进行定制。