432 lines
14 KiB
TypeScript
432 lines
14 KiB
TypeScript
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 }
|
||
);
|
||
}
|
||
}
|