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"; // 角度编辑API配置类型 type AngleEditConfig = { enabled?: boolean; falApiKey?: string; loraScale?: number; loraUrl?: string; inferenceSteps?: number; guidanceScale?: number; maxImageSize?: number; timeout?: number; }; // 角度预设配置(使用官方提示词格式) const ANGLE_PRESETS: Record = { // 汽车常用角度 "front": { azimuth: "front view", elevation: "eye-level shot", distance: "medium shot" }, "front-3/4": { azimuth: "front-right quarter view", elevation: "eye-level shot", distance: "medium shot" }, "side": { azimuth: "right side view", elevation: "eye-level shot", distance: "medium shot" }, "rear-3/4": { azimuth: "back-right quarter view", elevation: "eye-level shot", distance: "medium shot" }, "rear": { azimuth: "back view", elevation: "eye-level shot", distance: "medium shot" }, "overhead": { azimuth: "front view", elevation: "high-angle shot", distance: "wide shot" }, "low-front": { azimuth: "front view", elevation: "low-angle shot", distance: "close-up" }, "low-side": { azimuth: "right side view", elevation: "low-angle shot", distance: "medium shot" }, }; // 方位角映射(官方格式) const AZIMUTH_MAP: Record = { "front": "front view", "front-right": "front-right quarter view", "right": "right side view", "back-right": "back-right quarter view", "back": "back view", "back-left": "back-left quarter view", "left": "left side view", "front-left": "front-left quarter view", }; // 仰角映射(官方格式) const ELEVATION_MAP: Record = { "high": "high-angle shot", // 60° "elevated": "elevated shot", // 30° "eye-level": "eye-level shot", // 0° "low": "low-angle shot", // -30° }; // 距离映射(官方格式) const DISTANCE_MAP: Record = { "close": "close-up", // ×0.6 "medium": "medium shot", // ×1.0 "wide": "wide shot", // ×1.8 }; // 默认 LoRA 地址 const DEFAULT_LORA_URL = "https://huggingface.co/fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA/resolve/main/qwen-image-edit-2511-multiple-angles-lora.safetensors"; // 设置API路由最大执行时间为3分钟(180秒) export const maxDuration = 180; /** * 压缩图片生成缩略图 */ async function createThumbnail(buffer: Buffer): Promise { try { const sharp = await import("sharp"); const thumbnailBuffer = await sharp.default(buffer) .resize(500, null, { fit: "inside", withoutEnlargement: true, }) .jpeg({ quality: 80 }) .toBuffer(); console.log(`[Angle-Edit] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`); return thumbnailBuffer; } catch (error) { console.error("[Angle-Edit] Failed to create thumbnail:", error); return buffer; } } /** * 将 base64 图片上传到 S3 */ async function uploadBase64ToS3(base64Data: string, prefix: string = "angle-edit"): Promise<{ thumbnail: string; original: string }> { try { const buffer = Buffer.from(base64Data, "base64"); const timestamp = Date.now(); const randomStr = Math.random().toString(36).slice(2, 8); const thumbnailFilename = `${prefix}-thumb-${timestamp}-${randomStr}.png`; const originalFilename = `${prefix}-orig-${timestamp}-${randomStr}.png`; console.log(`[Angle-Edit] Original size: ${buffer.length} bytes`); const thumbnailBuffer = await createThumbnail(buffer); 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; console.log(`[Angle-Edit] Uploading thumbnail and original to S3...`); const [thumbnailResult, originalResult] = await Promise.all([ uploadFile(thumbnailFilename, thumbnailArrayBuffer), uploadFile(originalFilename, originalArrayBuffer), ]); console.log(`[Angle-Edit] Uploaded to S3:`); console.log(` - Thumbnail: ${thumbnailResult.url}`); console.log(` - Original: ${originalResult.url}`); return { thumbnail: thumbnailResult.url, original: originalResult.url, }; } catch (error) { console.error("[Angle-Edit] Failed to upload to S3:", error); throw new Error(`图片上传失败: ${error instanceof Error ? error.message : "未知错误"}`); } } export async function POST(request: NextRequest) { const startTime = Date.now(); try { // 1. 验证用户登录 const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } console.log("[Angle-Edit] ===== 收到请求 ====="); console.log("[Angle-Edit] 用户ID:", session.user.id); // 2. 从数据库获取配置 const angleEditConfig = await getSystemSetting({ key: "angle_edit_config" }); const config = (angleEditConfig?.value as AngleEditConfig) || {}; // 验证功能是否启用 if (!config.enabled) { return NextResponse.json( { error: "角度编辑功能未启用,请联系管理员在系统设置中开启" }, { status: 400 } ); } // 验证 API Key if (!config.falApiKey) { return NextResponse.json( { error: "fal.ai API Key 未配置,请联系管理员配置" }, { status: 500 } ); } console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale || 0.9); // 3. 解析请求 const body = await request.json(); const { image_url, image_base64, preset, azimuth, elevation, distance, lora_scale, } = body; if (!image_url && !image_base64) { return NextResponse.json({ error: "缺少图片数据" }, { status: 400 }); } // 使用请求中的 lora_scale 或配置中的默认值 const finalLoraScale = lora_scale ?? config.loraScale ?? 0.9; // 4. 构建角度参数 let angleParams: { azimuth: string; elevation: string; distance: string }; if (preset && ANGLE_PRESETS[preset]) { angleParams = ANGLE_PRESETS[preset]; console.log("[Angle-Edit] 使用预设:", preset); } else { angleParams = { azimuth: AZIMUTH_MAP[azimuth] || azimuth || "front view", elevation: ELEVATION_MAP[elevation] || elevation || "eye-level shot", distance: DISTANCE_MAP[distance] || distance || "medium shot", }; console.log("[Angle-Edit] 使用自定义参数:", angleParams); } // 5. 构建提示词 const prompt = ` ${angleParams.azimuth} ${angleParams.elevation} ${angleParams.distance}`; console.log("[Angle-Edit] 提示词:", prompt); // 6. 准备图片输入 let imageInput: string; if (image_url) { imageInput = image_url; } else { const mimeType = image_base64.startsWith("/9j/") ? "image/jpeg" : "image/png"; imageInput = `data:${mimeType};base64,${image_base64}`; } // 7. 动态导入 fal.ai SDK 并调用 API console.log("[Angle-Edit] 调用 fal.ai API..."); const fal = await import("@fal-ai/serverless-client"); fal.config({ credentials: config.falApiKey, }); const loraUrl = config.loraUrl || DEFAULT_LORA_URL; const result = await fal.subscribe("fal-ai/qwen-image-edit", { input: { image_url: imageInput, prompt: prompt, lora_url: loraUrl, lora_scale: finalLoraScale, num_inference_steps: config.inferenceSteps || 28, guidance_scale: config.guidanceScale || 3.5, }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { console.log("[Angle-Edit] 处理进度..."); } }, }); // 8. 处理结果 const outputImage = (result as { images?: Array<{ url?: string }> }).images?.[0]; if (!outputImage?.url) { throw new Error("API 未返回有效图片"); } const outputImageUrl = outputImage.url; console.log("[Angle-Edit] 输出图片URL:", outputImageUrl); // 9. 下载图片并上传到我们的 S3 const imageResponse = await fetch(outputImageUrl); if (!imageResponse.ok) { throw new Error(`下载输出图片失败: ${imageResponse.status}`); } const imageBuffer = Buffer.from(await imageResponse.arrayBuffer()); const imageBase64Result = imageBuffer.toString("base64"); const s3Result = await uploadBase64ToS3(imageBase64Result, "angle-edit"); console.log("[Angle-Edit] 已上传到S3:", s3Result.original); // 10. 保存到数据库 const processingTime = (Date.now() - startTime) / 1000; await db.insert(generatedImage).values({ userId: session.user.id, thumbnailUrl: s3Result.thumbnail, originalUrl: s3Result.original, prompt: `角度编辑: ${preset || `${azimuth}-${elevation}-${distance}`}`, modelId: "qwen-image-edit-angle", createdAt: new Date(), }); console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒"); // 11. 返回结果 return NextResponse.json({ success: true, image_url: s3Result.original, thumbnail_url: s3Result.thumbnail, image_base64: imageBase64Result, processing_time: processingTime, preset: preset, angles: angleParams, }); } catch (error) { console.error("[Angle-Edit] ❌ 错误:", error); return NextResponse.json( { error: error instanceof Error ? error.message : "角度编辑处理失败", }, { status: 500 } ); } }