import { NextRequest, NextResponse } from "next/server"; import { createMediaAssetRelation, getMediaAssetsByUserAndUrls, getSystemSetting, upsertMediaAsset, } 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; inferenceSteps?: number; guidanceScale?: number; maxImageSize?: number; timeout?: number; }; // 设置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 || 1); // 3. 解析请求 const body = await request.json(); const { image_url, image_base64, // 数值参数 (官方 API 格式) horizontal_angle, // 0-360 度 vertical_angle, // -30 到 90 度 zoom, // 0-10 lora_scale, } = body; if (!image_url && !image_base64) { return NextResponse.json({ error: "缺少图片数据" }, { status: 400 }); } // 使用请求中的参数或配置中的默认值 const finalLoraScale = lora_scale ?? config.loraScale ?? 1; const finalHorizontalAngle = horizontal_angle ?? 0; const finalVerticalAngle = vertical_angle ?? 0; const finalZoom = zoom ?? 5; console.log("[Angle-Edit] 角度参数:"); console.log(` - 水平角度: ${finalHorizontalAngle}°`); console.log(` - 垂直角度: ${finalVerticalAngle}°`); console.log(` - 缩放: ${finalZoom}`); console.log(` - LoRA强度: ${finalLoraScale}`); // 4. 准备图片输入 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}`; } // 5. 动态导入 fal.ai SDK 并调用官方多角度 API console.log("[Angle-Edit] 调用 fal.ai API (qwen-image-edit-2511-multiple-angles)..."); const fal = await import("@fal-ai/serverless-client"); fal.config({ credentials: config.falApiKey, }); // 使用官方多角度 API 端点 // 文档: https://fal.ai/models/fal-ai/qwen-image-edit-2511-multiple-angles const result = await fal.subscribe("fal-ai/qwen-image-edit-2511-multiple-angles", { input: { image_urls: [imageInput], // 官方使用 image_urls (数组) horizontal_angle: finalHorizontalAngle, // 水平旋转角度 0-360 vertical_angle: finalVerticalAngle, // 垂直角度 -30 到 90 zoom: finalZoom, // 缩放 0-10 lora_scale: finalLoraScale, // LoRA 强度 num_inference_steps: config.inferenceSteps || 28, guidance_scale: config.guidanceScale || 4.5, // 官方默认 4.5 enable_safety_checker: true, output_format: "png", }, logs: true, onQueueUpdate: (update) => { if (update.status === "IN_PROGRESS") { console.log("[Angle-Edit] 处理进度..."); } }, }); // 6. 处理结果 const apiResult = result as { images?: Array<{ url?: string }>; prompt?: string }; const outputImage = apiResult.images?.[0]; if (!outputImage?.url) { throw new Error("API 未返回有效图片"); } const outputImageUrl = outputImage.url; console.log("[Angle-Edit] 输出图片URL:", outputImageUrl); console.log("[Angle-Edit] API 构建的提示词:", apiResult.prompt); // 7. 下载图片并上传到我们的 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); // 8. 保存到数据库 const processingTime = (Date.now() - startTime) / 1000; const [savedImage] = await db.insert(generatedImage).values({ userId: session.user.id, thumbnailUrl: s3Result.thumbnail, originalUrl: s3Result.original, prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`, modelId: "qwen-image-edit-2511-multiple-angles", createdAt: new Date(), }).returning(); try { const sourceAssets = typeof image_url === "string" && image_url.trim() ? await getMediaAssetsByUserAndUrls({ userId: session.user.id, urls: [image_url], }) : []; const media = await upsertMediaAsset({ userId: session.user.id, mediaType: "image", sourceType: "edited", status: "ready", originalUrl: s3Result.original, previewUrl: s3Result.original, thumbnailUrl: s3Result.thumbnail, displayName: `角度编辑 ${finalHorizontalAngle}/${finalVerticalAngle}`.slice(0, 80), prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`, modelId: "qwen-image-edit-2511-multiple-angles", sourceTable: "GeneratedImage", sourceRowId: savedImage.id, primaryParentAssetId: sourceAssets[0]?.id ?? null, metadata: { horizontalAngle: finalHorizontalAngle, verticalAngle: finalVerticalAngle, zoom: finalZoom, }, }); if (media) { for (const sourceAsset of sourceAssets) { await createMediaAssetRelation({ userId: session.user.id, fromAssetId: sourceAsset.id, toAssetId: media.id, relationType: "edited_from", metadata: { horizontalAngle: finalHorizontalAngle, verticalAngle: finalVerticalAngle, zoom: finalZoom, }, }); } } } catch (assetError) { console.error("[Angle-Edit] ⚠️ MediaAsset 注册失败:", assetError); } console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒"); // 9. 返回结果 return NextResponse.json({ success: true, image_url: s3Result.original, thumbnail_url: s3Result.thumbnail, image_base64: imageBase64Result, processing_time: processingTime, angles: { horizontal_angle: finalHorizontalAngle, vertical_angle: finalVerticalAngle, zoom: finalZoom, }, prompt: apiResult.prompt, }); } catch (error) { console.error("[Angle-Edit] ❌ 错误:", error); // 输出详细的错误信息 if (error && typeof error === "object" && "status" in error) { const apiError = error as { status?: number; body?: unknown; message?: string }; console.error("[Angle-Edit] API 错误详情:"); console.error(" - Status:", apiError.status); console.error(" - Body:", JSON.stringify(apiError.body, null, 2)); console.error(" - Message:", apiError.message); if (apiError.status === 403) { return NextResponse.json( { error: "API 访问被拒绝 (403 Forbidden)。请检查 API Key 是否正确,以及是否有权限访问该模型。", details: apiError.body, }, { status: 403 } ); } } return NextResponse.json( { error: error instanceof Error ? error.message : "角度编辑处理失败", }, { status: 500 } ); } }