lovart3/app/api/rmbg/route.ts

432 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from "next/server";
import { getSystemSetting } from "@/lib/db/queries";
import { auth } from "@/app/(auth)/auth";
import { uploadFile } from "@/lib/storage/service";
import { db } from "@/lib/db";
import { generatedImage } from "@/lib/db/schema";
/**
* 压缩图片生成缩略图(使用 sharp)
*/
async function createThumbnail(buffer: Buffer, mimeType: string): Promise<Buffer> {
try {
const sharp = await import("sharp");
// 创建缩略图(宽度 500px,高度按比例,质量 80%)
const thumbnailBuffer = await sharp.default(buffer)
.resize(500, null, {
fit: "inside",
withoutEnlargement: true,
})
.png({ quality: 80 }) // PNG格式保持透明背景
.toBuffer();
console.log(`[RMBG] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
return thumbnailBuffer;
} catch (error) {
console.error("[RMBG] Failed to create thumbnail:", error);
// 如果失败,返回原图
return buffer;
}
}
/**
* 将图片 buffer 上传到 S3,返回缩略图和原图的 URL
*/
async function uploadBufferToS3(buffer: Buffer, mimeType: string = "image/png"): Promise<{ thumbnail: string; original: string }> {
try {
// 生成文件名
const ext = mimeType.split("/")[1] || "png";
const timestamp = Date.now();
const randomStr = Math.random().toString(36).slice(2, 8);
const thumbnailFilename = `rmbg-thumb-${timestamp}-${randomStr}.${ext}`;
const originalFilename = `rmbg-orig-${timestamp}-${randomStr}.${ext}`;
console.log(`[RMBG] Original size: ${buffer.length} bytes`);
// 生成缩略图
const thumbnailBuffer = await createThumbnail(buffer, mimeType);
// 转换为 ArrayBuffer
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
thumbnailBuffer.byteOffset,
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
) as ArrayBuffer;
const originalArrayBuffer = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
// 并行上传缩略图和原图到 S3
console.log(`[RMBG] Uploading thumbnail and original to S3...`);
const [thumbnailResult, originalResult] = await Promise.all([
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
uploadFile(originalFilename, originalArrayBuffer),
]);
console.log(`[RMBG] Uploaded to S3:`);
console.log(` - Thumbnail: ${thumbnailResult.url}`);
console.log(` - Original: ${originalResult.url}`);
return {
thumbnail: thumbnailResult.url,
original: originalResult.url,
};
} catch (error) {
console.error("[RMBG] Failed to upload to S3:", error);
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
}
}
/**
* 将 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路由
* 从数据库读取API配置,支持 RMBG-2.0 和 PhotoRoom 两种服务
*/
export async function POST(request: NextRequest) {
try {
// 0. 获取用户会话
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "未授权,请先登录" },
{ status: 401 }
);
}
const userId = session.user.id;
// 1. 从数据库获取抠图API配置
const rmbgConfig = await getSystemSetting({ key: "rmbg_api_config" });
if (!rmbgConfig || !rmbgConfig.value) {
return NextResponse.json(
{ error: "抠图API配置未设置,请在系统设置中配置" },
{ status: 400 }
);
}
const config = rmbgConfig.value as {
provider?: "rmbg" | "photoroom";
apiUrl?: string;
apiKey?: string;
photoroomApiKey?: string;
};
const provider = config.provider || "rmbg";
// 验证配置
if (provider === "rmbg" && !config.apiUrl) {
return NextResponse.json(
{ error: "RMBG API地址未配置" },
{ status: 400 }
);
}
if (provider === "photoroom" && !config.photoroomApiKey) {
return NextResponse.json(
{ error: "PhotoRoom API密钥未配置" },
{ status: 400 }
);
}
// 2. 获取请求体
const contentType = request.headers.get("content-type") || "";
let imageBuffer: Buffer;
let imageBase64: string;
let prompt = "";
let chatId: string | null = null;
let messageId: string | null = null;
if (contentType.includes("application/json")) {
const body = await request.json();
prompt = body.prompt || "抠图处理";
chatId = body.chatId || null;
messageId = body.messageId || null;
if (body.imageUrl) {
console.log(`[RMBG] 从 URL 获取图片: ${body.imageUrl}`);
try {
const imageResponse = await fetch(body.imageUrl);
if (!imageResponse.ok) {
return NextResponse.json(
{ error: `无法获取图片: ${imageResponse.status} ${imageResponse.statusText}` },
{ status: 400 }
);
}
const arrayBuffer = await imageResponse.arrayBuffer();
imageBuffer = Buffer.from(arrayBuffer);
imageBase64 = imageBuffer.toString("base64");
console.log(`[RMBG] 图片已获取,大小: ${imageBuffer.length} bytes`);
} catch (fetchError) {
console.error("[RMBG] 获取图片失败:", fetchError);
return NextResponse.json(
{ error: "无法获取图片,请检查图片链接是否有效" },
{ status: 400 }
);
}
} else if (body.image_base64) {
imageBase64 = body.image_base64;
imageBuffer = Buffer.from(imageBase64, "base64");
} else {
return NextResponse.json(
{ error: "缺少 imageUrl 或 image_base64 参数" },
{ status: 400 }
);
}
} else if (contentType.includes("multipart/form-data")) {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json(
{ error: "缺少file参数" },
{ status: 400 }
);
}
const arrayBuffer = await file.arrayBuffer();
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;
} else {
return NextResponse.json(
{ error: "不支持的Content-Type,请使用application/json或multipart/form-data" },
{ status: 400 }
);
}
// 3. 根据提供商调用不同的API
console.log(`[RMBG] Using provider: ${provider}`);
let resultBuffer: Buffer;
let resultBase64: string;
let processingTime: number;
if (provider === "photoroom") {
// 使用 PhotoRoom API
const result = await processWithPhotoRoom(imageBuffer, config.photoroomApiKey!);
if (!result.success || !result.imageBuffer) {
return NextResponse.json(
{ error: "抠图处理失败", detail: result.error },
{ status: 500 }
);
}
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,
});
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;
}
// 4. 上传处理后的图片到S3
let thumbnailUrl: string;
let originalUrl: string;
try {
const uploadResult = await uploadBufferToS3(resultBuffer, "image/png");
thumbnailUrl = uploadResult.thumbnail;
originalUrl = uploadResult.original;
console.log(`[RMBG] ✅ Images uploaded to S3 successfully`);
} catch (uploadError) {
console.error("[RMBG] ❌ Failed to upload to S3:", uploadError);
return NextResponse.json(
{
error: "图片上传到存储桶失败",
detail: uploadError instanceof Error ? uploadError.message : "未知错误"
},
{ status: 500 }
);
}
// 5. 保存图片记录到数据库
try {
const [savedImage] = await db.insert(generatedImage).values({
userId,
chatId: chatId || null,
messageId: messageId || null,
thumbnailUrl,
originalUrl,
imageSize: "2K",
prompt: prompt || "抠图处理",
modelId: provider === "photoroom" ? "photoroom-hd" : "rmbg-2.0",
}).returning();
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
} catch (dbError) {
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
}
// 6. 返回结果
return NextResponse.json({
success: true,
image_base64: resultBase64,
thumbnail_url: thumbnailUrl,
original_url: originalUrl,
processing_time: processingTime,
provider: provider,
});
} catch (error) {
console.error("[RMBG API] 处理错误:", error);
return NextResponse.json(
{ error: "服务器内部错误", detail: error instanceof Error ? error.message : "未知错误" },
{ status: 500 }
);
}
}