chore: govern angle edit api logging
This commit is contained in:
parent
0768f84802
commit
36f3de8321
@ -9,6 +9,9 @@ import { auth } from "@/app/(auth)/auth";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { db } from "@/lib/db";
|
||||
import { generatedImage } from "@/lib/db/schema";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
|
||||
const logger = createLogger("angle-edit-api");
|
||||
|
||||
// 角度编辑API配置类型
|
||||
type AngleEditConfig = {
|
||||
@ -30,7 +33,7 @@ export const maxDuration = 180;
|
||||
async function createThumbnail(buffer: Buffer): Promise<Buffer> {
|
||||
try {
|
||||
const sharp = await import("sharp");
|
||||
|
||||
|
||||
const thumbnailBuffer = await sharp.default(buffer)
|
||||
.resize(500, null, {
|
||||
fit: "inside",
|
||||
@ -38,11 +41,14 @@ async function createThumbnail(buffer: Buffer): Promise<Buffer> {
|
||||
})
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
|
||||
console.log(`[Angle-Edit] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
|
||||
|
||||
logger.debug("thumbnail_created", {
|
||||
originalBytes: buffer.length,
|
||||
thumbnailBytes: thumbnailBuffer.length,
|
||||
});
|
||||
return thumbnailBuffer;
|
||||
} catch (error) {
|
||||
console.error("[Angle-Edit] Failed to create thumbnail:", error);
|
||||
logger.warn("thumbnail_creation_failed", error);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@ -57,43 +63,48 @@ async function uploadBase64ToS3(base64Data: string, prefix: string = "angle-edit
|
||||
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`);
|
||||
|
||||
|
||||
logger.debug("source_image_prepared", { originalBytes: buffer.length });
|
||||
|
||||
const thumbnailBuffer = await createThumbnail(buffer);
|
||||
|
||||
|
||||
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
|
||||
thumbnailBuffer.byteOffset,
|
||||
thumbnailBuffer.byteOffset,
|
||||
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
|
||||
) as ArrayBuffer;
|
||||
const originalArrayBuffer = buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength
|
||||
) as ArrayBuffer;
|
||||
|
||||
console.log(`[Angle-Edit] Uploading thumbnail and original to S3...`);
|
||||
|
||||
logger.debug("storage_upload_started", {
|
||||
hasOriginal: originalArrayBuffer.byteLength > 0,
|
||||
hasThumbnail: thumbnailArrayBuffer.byteLength > 0,
|
||||
prefix,
|
||||
});
|
||||
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}`);
|
||||
|
||||
|
||||
logger.info("storage_upload_completed", {
|
||||
originalUrl: originalResult.url,
|
||||
thumbnailUrl: thumbnailResult.url,
|
||||
});
|
||||
|
||||
return {
|
||||
thumbnail: thumbnailResult.url,
|
||||
original: originalResult.url,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[Angle-Edit] Failed to upload to S3:", error);
|
||||
logger.error("storage_upload_failed", 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();
|
||||
@ -101,8 +112,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
console.log("[Angle-Edit] ===== 收到请求 =====");
|
||||
console.log("[Angle-Edit] 用户ID:", session.user.id);
|
||||
logger.info("request_received", { authenticated: true });
|
||||
|
||||
// 2. 从数据库获取配置
|
||||
const angleEditConfig = await getSystemSetting({ key: "angle_edit_config" });
|
||||
@ -124,7 +134,11 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale || 1);
|
||||
logger.debug("config_loaded", {
|
||||
enabled: Boolean(config.enabled),
|
||||
hasApiKey: Boolean(config.falApiKey),
|
||||
loraScale: config.loraScale || 1,
|
||||
});
|
||||
|
||||
// 3. 解析请求
|
||||
const body = await request.json();
|
||||
@ -132,9 +146,9 @@ export async function POST(request: NextRequest) {
|
||||
image_url,
|
||||
image_base64,
|
||||
// 数值参数 (官方 API 格式)
|
||||
horizontal_angle, // 0-360 度
|
||||
vertical_angle, // -30 到 90 度
|
||||
zoom, // 0-10
|
||||
horizontal_angle, // 0-360 度
|
||||
vertical_angle, // -30 到 90 度
|
||||
zoom, // 0-10
|
||||
lora_scale,
|
||||
} = body;
|
||||
|
||||
@ -148,11 +162,14 @@ export async function POST(request: NextRequest) {
|
||||
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}`);
|
||||
logger.info("request_parameters_normalized", {
|
||||
hasBase64Input: Boolean(image_base64),
|
||||
hasUrlInput: Boolean(image_url),
|
||||
horizontalAngle: finalHorizontalAngle,
|
||||
loraScale: finalLoraScale,
|
||||
verticalAngle: finalVerticalAngle,
|
||||
zoom: finalZoom,
|
||||
});
|
||||
|
||||
// 4. 准备图片输入
|
||||
let imageInput: string;
|
||||
@ -164,10 +181,12 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 5. 动态导入 fal.ai SDK 并调用官方多角度 API
|
||||
console.log("[Angle-Edit] 调用 fal.ai API (qwen-image-edit-2511-multiple-angles)...");
|
||||
|
||||
logger.info("provider_call_started", {
|
||||
modelId: "fal-ai/qwen-image-edit-2511-multiple-angles",
|
||||
});
|
||||
|
||||
const fal = await import("@fal-ai/serverless-client");
|
||||
|
||||
|
||||
fal.config({
|
||||
credentials: config.falApiKey,
|
||||
});
|
||||
@ -176,20 +195,20 @@ export async function POST(request: NextRequest) {
|
||||
// 文档: 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 强度
|
||||
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
|
||||
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] 处理进度...");
|
||||
logger.debug("provider_progress", { status: update.status });
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -202,8 +221,11 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const outputImageUrl = outputImage.url;
|
||||
console.log("[Angle-Edit] 输出图片URL:", outputImageUrl);
|
||||
console.log("[Angle-Edit] API 构建的提示词:", apiResult.prompt);
|
||||
logger.info("provider_call_completed", {
|
||||
hasGeneratedPrompt: Boolean(apiResult.prompt),
|
||||
imageCount: apiResult.images?.length ?? 0,
|
||||
outputImageUrl,
|
||||
});
|
||||
|
||||
// 7. 下载图片并上传到我们的 S3
|
||||
const imageResponse = await fetch(outputImageUrl);
|
||||
@ -214,19 +236,25 @@ export async function POST(request: NextRequest) {
|
||||
const imageBase64Result = imageBuffer.toString("base64");
|
||||
|
||||
const s3Result = await uploadBase64ToS3(imageBase64Result, "angle-edit");
|
||||
console.log("[Angle-Edit] 已上传到S3:", s3Result.original);
|
||||
logger.info("result_image_persisted", {
|
||||
originalUrl: s3Result.original,
|
||||
thumbnailUrl: s3Result.thumbnail,
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
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 =
|
||||
@ -272,10 +300,12 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
} catch (assetError) {
|
||||
console.error("[Angle-Edit] ⚠️ MediaAsset 注册失败:", assetError);
|
||||
logger.warn("media_asset_registration_failed", assetError);
|
||||
}
|
||||
|
||||
console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒");
|
||||
logger.info("request_completed", {
|
||||
processingSeconds: Number(processingTime.toFixed(2)),
|
||||
});
|
||||
|
||||
// 9. 返回结果
|
||||
return NextResponse.json({
|
||||
@ -293,19 +323,21 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Angle-Edit] ❌ 错误:", error);
|
||||
|
||||
logger.error("request_failed", 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);
|
||||
|
||||
logger.warn("provider_error_summary", {
|
||||
bodyType: typeof apiError.body,
|
||||
hasBody: apiError.body != null,
|
||||
message: apiError.message,
|
||||
status: apiError.status,
|
||||
});
|
||||
|
||||
if (apiError.status === 403) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
{
|
||||
error: "API 访问被拒绝 (403 Forbidden)。请检查 API Key 是否正确,以及是否有权限访问该模型。",
|
||||
details: apiError.body,
|
||||
},
|
||||
@ -313,9 +345,9 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
{
|
||||
error: error instanceof Error ? error.message : "角度编辑处理失败",
|
||||
},
|
||||
{ status: 500 }
|
||||
|
||||
@ -31,6 +31,7 @@ const files = [
|
||||
"app/api/canvas/text-detect/route.ts",
|
||||
"app/api/canvas/detail-plan/route.ts",
|
||||
"app/api/scene-fusion/route.ts",
|
||||
"app/api/angle-edit/route.ts",
|
||||
"app/api/pick-object/route.ts",
|
||||
"app/api/canvas/world/upload/route.ts",
|
||||
"app/api/canvas/3d/tasks/route.ts",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user