-
-
setRmbgConfig({ ...rmbgConfig, apiUrl: e.target.value })}
- placeholder="http://156.225.27.52:8000"
- />
-
- 请输入完整的API地址,例如: http://156.225.27.52:8000
-
-
-
-
-
setRmbgConfig({ ...rmbgConfig, apiKey: e.target.value })}
- placeholder="如果API需要密钥,请在此输入"
- />
-
- 如果API需要认证,请填写API密钥(可选)
-
-
-
-
API文档说明
-
-
• API地址应指向RMBG-2.0服务的根地址
-
• 系统会自动调用 /rmbg/base64 接口进行抠图处理
-
• 支持的图片格式:JPG, PNG, JPEG等常见格式
-
• 输出格式:PNG格式的透明背景图片
+
+
+
+
+
+
+
+ {/* RMBG 配置 */}
+ {rmbgConfig.provider === "rmbg" && (
+ <>
+
RMBG-2.0 配置
+
+
+
setRmbgConfig({ ...rmbgConfig, apiUrl: e.target.value })}
+ placeholder="http://156.225.27.52:8000"
+ />
+
+ 请输入完整的API地址,例如: http://156.225.27.52:8000
+
+
+
+
+ setRmbgConfig({ ...rmbgConfig, apiKey: e.target.value })}
+ placeholder="如果API需要密钥,请在此输入"
+ />
+
+
+
RMBG-2.0 说明
+
+
• 需要自行部署RMBG-2.0服务
+
• 系统会自动调用 /rmbg/base64 接口进行抠图处理
+
• 支持的图片格式:JPG, PNG, JPEG等常见格式
+
+
+ >
+ )}
+
+ {/* PhotoRoom 配置 */}
+ {rmbgConfig.provider === "photoroom" && (
+ <>
+
PhotoRoom 配置
+
+
+
setRmbgConfig({ ...rmbgConfig, photoroomApiKey: e.target.value })}
+ placeholder="输入PhotoRoom API密钥"
+ />
+
+ 在 PhotoRoom 开发者平台获取API密钥
+
+
+
+
PhotoRoom 说明
+
+
• 使用 PhotoRoom 云服务进行高清抠图
+
• 支持 HD 高清背景移除,效果更好
+
• API文档:https://www.photoroom.com/api
+
• 按调用次数计费,请注意用量
+
+
+ >
+ )}
+
diff --git a/app/api/rmbg/route.ts b/app/api/rmbg/route.ts
index d0c822eab..ff44295b0 100644
--- a/app/api/rmbg/route.ts
+++ b/app/api/rmbg/route.ts
@@ -31,13 +31,10 @@ async function createThumbnail(buffer: Buffer, mimeType: string): Promise
{
+async function uploadBufferToS3(buffer: Buffer, mimeType: string = "image/png"): Promise<{ thumbnail: string; original: string }> {
try {
- // 解析 base64 数据
- const buffer = Buffer.from(base64Data, "base64");
-
// 生成文件名
const ext = mimeType.split("/")[1] || "png";
const timestamp = Date.now();
@@ -81,10 +78,146 @@ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/pn
}
}
+/**
+ * 将 base64 图片上传到 S3,返回缩略图和原图的 URL
+ */
+async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/png"): Promise<{ thumbnail: string; original: string }> {
+ const buffer = Buffer.from(base64Data, "base64");
+ return uploadBufferToS3(buffer, mimeType);
+}
+
+/**
+ * 使用 RMBG-2.0 API 进行抠图
+ */
+async function processWithRMBG(
+ imageBase64: string,
+ config: { apiUrl: string; apiKey?: string }
+): Promise<{ success: boolean; image_base64?: string; processing_time?: number; error?: string }> {
+ const apiUrl = config.apiUrl.endsWith("/")
+ ? `${config.apiUrl}rmbg/base64`
+ : `${config.apiUrl}/rmbg/base64`;
+
+ const requestBody = {
+ image_base64: imageBase64,
+ };
+
+ const headers: HeadersInit = {
+ "Content-Type": "application/json",
+ };
+
+ if (config.apiKey) {
+ headers["X-API-Key"] = config.apiKey;
+ }
+
+ const startTime = Date.now();
+
+ // 添加超时控制(30秒)
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
+
+ try {
+ const response = await fetch(apiUrl, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(requestBody),
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+ const processingTime = (Date.now() - startTime) / 1000;
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error("[RMBG API] 请求失败:", { status: response.status, error: errorText });
+ return { success: false, error: errorText || response.statusText };
+ }
+
+ const result = await response.json();
+
+ if (!result.success || !result.image_base64) {
+ return { success: false, error: result.detail || "未知错误" };
+ }
+
+ return {
+ success: true,
+ image_base64: result.image_base64,
+ processing_time: result.processing_time || processingTime,
+ };
+ } catch (error) {
+ clearTimeout(timeoutId);
+ if (error instanceof Error && error.name === 'AbortError') {
+ return { success: false, error: "请求超时(30秒)" };
+ }
+ return { success: false, error: error instanceof Error ? error.message : "未知错误" };
+ }
+}
+
+/**
+ * 使用 PhotoRoom API 进行抠图
+ */
+async function processWithPhotoRoom(
+ imageBuffer: Buffer,
+ apiKey: string
+): Promise<{ success: boolean; imageBuffer?: Buffer; processing_time?: number; error?: string }> {
+ const startTime = Date.now();
+
+ try {
+ // 构建 FormData
+ const formData = new FormData();
+ // 将 Buffer 转换为 Uint8Array 以避免类型问题
+ const uint8Array = new Uint8Array(imageBuffer);
+ const blob = new Blob([uint8Array], { type: "image/png" });
+ formData.append("imageFile", blob, "image.png");
+ formData.append("referenceBox", "originalImage");
+
+ console.log("[PhotoRoom] Sending request to API...");
+
+ // 添加超时控制(60秒,PhotoRoom可能需要更长时间)
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 60000);
+
+ const response = await fetch("https://image-api.photoroom.com/v2/edit", {
+ method: "POST",
+ headers: {
+ "x-api-key": apiKey,
+ "pr-hd-background-removal": "auto",
+ },
+ body: formData,
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+ const processingTime = (Date.now() - startTime) / 1000;
+
+ console.log("[PhotoRoom] Response status:", response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error("[PhotoRoom API] 请求失败:", { status: response.status, error: errorText });
+ return { success: false, error: `PhotoRoom API错误: ${response.status} - ${errorText}` };
+ }
+
+ // PhotoRoom 直接返回图片二进制数据
+ const resultBuffer = Buffer.from(await response.arrayBuffer());
+ console.log("[PhotoRoom] Received image buffer, size:", resultBuffer.length, "bytes");
+
+ return {
+ success: true,
+ imageBuffer: resultBuffer,
+ processing_time: processingTime,
+ };
+ } catch (error) {
+ if (error instanceof Error && error.name === 'AbortError') {
+ return { success: false, error: "PhotoRoom请求超时(60秒)" };
+ }
+ console.error("[PhotoRoom] Error:", error);
+ return { success: false, error: error instanceof Error ? error.message : "未知错误" };
+ }
+}
+
/**
* 抠图API路由
- * 从数据库读取RMBG API配置,调用外部API进行抠图处理
- * 将处理后的图片上传到S3并保存到数据库
+ * 从数据库读取API配置,支持 RMBG-2.0 和 PhotoRoom 两种服务
*/
export async function POST(request: NextRequest) {
try {
@@ -98,7 +231,8 @@ export async function POST(request: NextRequest) {
}
const userId = session.user.id;
- // 1. 从数据库获取RMBG API配置
+
+ // 1. 从数据库获取抠图API配置
const rmbgConfig = await getSystemSetting({ key: "rmbg_api_config" });
if (!rmbgConfig || !rmbgConfig.value) {
@@ -109,24 +243,37 @@ export async function POST(request: NextRequest) {
}
const config = rmbgConfig.value as {
+ provider?: "rmbg" | "photoroom";
apiUrl?: string;
apiKey?: string;
+ photoroomApiKey?: string;
};
- if (!config.apiUrl) {
+ const provider = config.provider || "rmbg";
+
+ // 验证配置
+ if (provider === "rmbg" && !config.apiUrl) {
return NextResponse.json(
- { error: "抠图API地址未配置" },
+ { error: "RMBG API地址未配置" },
+ { status: 400 }
+ );
+ }
+
+ if (provider === "photoroom" && !config.photoroomApiKey) {
+ return NextResponse.json(
+ { error: "PhotoRoom API密钥未配置" },
{ status: 400 }
);
}
- // 2. 获取请求体(支持 imageUrl、image_base64 或 FormData)
+ // 2. 获取请求体
const contentType = request.headers.get("content-type") || "";
+ let imageBuffer: Buffer;
let imageBase64: string;
- let prompt = ""; // 用户提示词(可选)
- let chatId: string | null = null; // 聊天ID(可选)
- let messageId: string | null = null; // 消息ID(可选)
+ let prompt = "";
+ let chatId: string | null = null;
+ let messageId: string | null = null;
if (contentType.includes("application/json")) {
const body = await request.json();
@@ -134,9 +281,7 @@ export async function POST(request: NextRequest) {
chatId = body.chatId || null;
messageId = body.messageId || null;
- // 优先使用 imageUrl,其次使用 image_base64
if (body.imageUrl) {
- // 从 URL 获取图片并转换为 Base64
console.log(`[RMBG] 从 URL 获取图片: ${body.imageUrl}`);
try {
const imageResponse = await fetch(body.imageUrl);
@@ -147,9 +292,9 @@ export async function POST(request: NextRequest) {
);
}
const arrayBuffer = await imageResponse.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
- imageBase64 = buffer.toString("base64");
- console.log(`[RMBG] 图片已转换为 Base64,大小: ${buffer.length} bytes`);
+ imageBuffer = Buffer.from(arrayBuffer);
+ imageBase64 = imageBuffer.toString("base64");
+ console.log(`[RMBG] 图片已获取,大小: ${imageBuffer.length} bytes`);
} catch (fetchError) {
console.error("[RMBG] 获取图片失败:", fetchError);
return NextResponse.json(
@@ -159,6 +304,7 @@ export async function POST(request: NextRequest) {
}
} else if (body.image_base64) {
imageBase64 = body.image_base64;
+ imageBuffer = Buffer.from(imageBase64, "base64");
} else {
return NextResponse.json(
{ error: "缺少 imageUrl 或 image_base64 参数" },
@@ -166,7 +312,6 @@ export async function POST(request: NextRequest) {
);
}
} else if (contentType.includes("multipart/form-data")) {
- // FormData格式:文件上传
const formData = await request.formData();
const file = formData.get("file") as File;
@@ -177,10 +322,9 @@ export async function POST(request: NextRequest) {
);
}
- // 将文件转换为Base64
const arrayBuffer = await file.arrayBuffer();
- const buffer = Buffer.from(arrayBuffer);
- imageBase64 = buffer.toString("base64");
+ imageBuffer = Buffer.from(arrayBuffer);
+ imageBase64 = imageBuffer.toString("base64");
prompt = (formData.get("prompt") as string) || "抠图处理";
chatId = (formData.get("chatId") as string) || null;
messageId = (formData.get("messageId") as string) || null;
@@ -191,134 +335,52 @@ export async function POST(request: NextRequest) {
);
}
- // 3. 调用RMBG API
- const apiUrl = config.apiUrl.endsWith("/")
- ? `${config.apiUrl}rmbg/base64`
- : `${config.apiUrl}/rmbg/base64`;
-
- const requestBody = {
- image_base64: imageBase64,
- };
-
- const headers: HeadersInit = {
- "Content-Type": "application/json",
- };
-
- // 如果配置了API Key,添加到请求头(根据RMBG API文档要求使用X-API-Key)
- if (config.apiKey) {
- headers["X-API-Key"] = config.apiKey;
- }
-
- const startTime = Date.now();
- let response: Response;
+ // 3. 根据提供商调用不同的API
+ console.log(`[RMBG] Using provider: ${provider}`);
- try {
- // 添加超时控制(30秒)
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 30000);
+ let resultBuffer: Buffer;
+ let resultBase64: string;
+ let processingTime: number;
+
+ if (provider === "photoroom") {
+ // 使用 PhotoRoom API
+ const result = await processWithPhotoRoom(imageBuffer, config.photoroomApiKey!);
- response = await fetch(apiUrl, {
- method: "POST",
- headers,
- body: JSON.stringify(requestBody),
- signal: controller.signal,
- });
-
- clearTimeout(timeoutId);
- } catch (fetchError) {
- const processingTime = (Date.now() - startTime) / 1000;
- console.error("[RMBG API] 网络请求失败:", {
- error: fetchError,
- apiUrl,
- processingTime,
- });
-
- // 检查是否是网络连接错误
- if (fetchError instanceof Error) {
- // 检查超时错误
- if (fetchError.name === 'AbortError') {
- return NextResponse.json(
- {
- error: "抠图API请求超时",
- detail: "请求超过30秒未响应,请检查API服务器状态或网络连接",
- },
- { status: 504 }
- );
- }
-
- // 检查错误消息和cause中的网络错误
- const errorMessage = fetchError.message || '';
- const errorCause = (fetchError as any).cause;
- const causeMessage = errorCause?.message || '';
- const causeCode = errorCause?.code || '';
-
- // 检查各种网络连接错误
- if (errorMessage.includes('EHOSTUNREACH') ||
- errorMessage.includes('ENOTFOUND') ||
- errorMessage.includes('ECONNREFUSED') ||
- errorMessage.includes('No route to host') ||
- causeMessage.includes('EHOSTUNREACH') ||
- causeMessage.includes('ENOTFOUND') ||
- causeMessage.includes('ECONNREFUSED') ||
- causeMessage.includes('No route to host') ||
- causeCode === 'EHOSTUNREACH' ||
- causeCode === 'ENOTFOUND' ||
- causeCode === 'ECONNREFUSED') {
- return NextResponse.json(
- {
- error: "无法连接到抠图API服务器",
- detail: `服务器无法访问 ${apiUrl},请检查:\n1. API服务器是否在线\n2. 网络连接是否正常\n3. 防火墙设置是否正确\n4. 端口8000是否已开放`,
- },
- { status: 503 }
- );
- }
+ if (!result.success || !result.imageBuffer) {
+ return NextResponse.json(
+ { error: "抠图处理失败", detail: result.error },
+ { status: 500 }
+ );
}
- return NextResponse.json(
- {
- error: "抠图API请求失败",
- detail: fetchError instanceof Error ? fetchError.message : "未知网络错误",
- },
- { status: 500 }
- );
- }
-
- const processingTime = (Date.now() - startTime) / 1000;
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error("[RMBG API] 请求失败:", {
- status: response.status,
- statusText: response.statusText,
- error: errorText,
- apiUrl,
+ resultBuffer = result.imageBuffer;
+ resultBase64 = resultBuffer.toString("base64");
+ processingTime = result.processing_time || 0;
+ } else {
+ // 使用 RMBG-2.0 API
+ const result = await processWithRMBG(imageBase64, {
+ apiUrl: config.apiUrl!,
+ apiKey: config.apiKey,
});
- return NextResponse.json(
- {
- error: "抠图API请求失败",
- detail: errorText || response.statusText,
- },
- { status: response.status }
- );
+ if (!result.success || !result.image_base64) {
+ return NextResponse.json(
+ { error: "抠图处理失败", detail: result.error },
+ { status: 500 }
+ );
+ }
+
+ resultBase64 = result.image_base64;
+ resultBuffer = Buffer.from(resultBase64, "base64");
+ processingTime = result.processing_time || 0;
}
- const result = await response.json();
-
- // 4. 处理返回结果
- if (!result.success || !result.image_base64) {
- return NextResponse.json(
- { error: "抠图处理失败", detail: result.detail || "未知错误" },
- { status: 500 }
- );
- }
-
- // 5. 上传处理后的图片到S3存储桶
+ // 4. 上传处理后的图片到S3
let thumbnailUrl: string;
let originalUrl: string;
try {
- const uploadResult = await uploadBase64ToS3(result.image_base64, "image/png");
+ const uploadResult = await uploadBufferToS3(resultBuffer, "image/png");
thumbnailUrl = uploadResult.thumbnail;
originalUrl = uploadResult.original;
console.log(`[RMBG] ✅ Images uploaded to S3 successfully`);
@@ -333,7 +395,7 @@ export async function POST(request: NextRequest) {
);
}
- // 6. 保存图片记录到数据库(相册和生图记录)
+ // 5. 保存图片记录到数据库
try {
const [savedImage] = await db.insert(generatedImage).values({
userId,
@@ -341,31 +403,23 @@ export async function POST(request: NextRequest) {
messageId: messageId || null,
thumbnailUrl,
originalUrl,
- imageSize: "2K", // 默认尺寸
+ imageSize: "2K",
prompt: prompt || "抠图处理",
- modelId: "rmbg-2.0", // 标记为抠图处理
+ modelId: provider === "photoroom" ? "photoroom-hd" : "rmbg-2.0",
}).returning();
- console.log(`[RMBG] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
+ console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
} catch (dbError) {
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
- // 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问)
- // 如果希望严格保证数据一致性,可以取消注释下面的代码来返回错误
- // return NextResponse.json(
- // {
- // error: "图片已处理但保存到相册失败",
- // detail: dbError instanceof Error ? dbError.message : "未知错误"
- // },
- // { status: 500 }
- // );
}
- // 7. 返回结果(包含S3 URL和Base64,前端可以选择使用)
+ // 6. 返回结果
return NextResponse.json({
success: true,
- image_base64: result.image_base64, // 保留Base64用于前端立即显示
- thumbnail_url: thumbnailUrl, // S3缩略图URL
- original_url: originalUrl, // S3原图URL
- processing_time: result.processing_time || processingTime,
+ image_base64: resultBase64,
+ thumbnail_url: thumbnailUrl,
+ original_url: originalUrl,
+ processing_time: processingTime,
+ provider: provider,
});
} catch (error) {
console.error("[RMBG API] 处理错误:", error);
@@ -375,4 +429,3 @@ export async function POST(request: NextRequest) {
);
}
}
-