@@ -3592,7 +3556,7 @@ export function AdminSettingsClient() {
id="guidanceScale"
type="number"
value={angleEditConfig.guidanceScale}
- onChange={(e) => setAngleEditConfig({ ...angleEditConfig, guidanceScale: Number.parseFloat(e.target.value) || 3.5 })}
+ onChange={(e) => setAngleEditConfig({ ...angleEditConfig, guidanceScale: Number.parseFloat(e.target.value) || 4.5 })}
min={1}
max={10}
step={0.5}
diff --git a/app/api/angle-edit/route.ts b/app/api/angle-edit/route.ts
index 4bfabeb..ec34afc 100644
--- a/app/api/angle-edit/route.ts
+++ b/app/api/angle-edit/route.ts
@@ -10,57 +10,12 @@ 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;
@@ -164,17 +119,17 @@ export async function POST(request: NextRequest) {
);
}
- console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale || 0.9);
+ console.log("[Angle-Edit] 配置加载成功,LoRA强度:", config.loraScale || 1);
// 3. 解析请求
const body = await request.json();
const {
image_url,
image_base64,
- preset,
- azimuth,
- elevation,
- distance,
+ // 数值参数 (官方 API 格式)
+ horizontal_angle, // 0-360 度
+ vertical_angle, // -30 到 90 度
+ zoom, // 0-10
lora_scale,
} = body;
@@ -182,29 +137,19 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "缺少图片数据" }, { status: 400 });
}
- // 使用请求中的 lora_scale 或配置中的默认值
- const finalLoraScale = lora_scale ?? config.loraScale ?? 0.9;
+ // 使用请求中的参数或配置中的默认值
+ const finalLoraScale = lora_scale ?? config.loraScale ?? 1;
+ const finalHorizontalAngle = horizontal_angle ?? 0;
+ const finalVerticalAngle = vertical_angle ?? 0;
+ const finalZoom = zoom ?? 5;
- // 4. 构建角度参数
- let angleParams: { azimuth: string; elevation: string; distance: string };
+ console.log("[Angle-Edit] 角度参数:");
+ console.log(` - 水平角度: ${finalHorizontalAngle}°`);
+ console.log(` - 垂直角度: ${finalVerticalAngle}°`);
+ console.log(` - 缩放: ${finalZoom}`);
+ console.log(` - LoRA强度: ${finalLoraScale}`);
- 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. 准备图片输入
+ // 4. 准备图片输入
let imageInput: string;
if (image_url) {
imageInput = image_url;
@@ -213,8 +158,8 @@ export async function POST(request: NextRequest) {
imageInput = `data:${mimeType};base64,${image_base64}`;
}
- // 7. 动态导入 fal.ai SDK 并调用 API
- console.log("[Angle-Edit] 调用 fal.ai API...");
+ // 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");
@@ -222,16 +167,19 @@ export async function POST(request: NextRequest) {
credentials: config.falApiKey,
});
- const loraUrl = config.loraUrl || DEFAULT_LORA_URL;
-
- const result = await fal.subscribe("fal-ai/qwen-image-edit", {
+ // 使用官方多角度 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_url: imageInput,
- prompt: prompt,
- lora_url: loraUrl,
- lora_scale: finalLoraScale,
+ 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 || 3.5,
+ guidance_scale: config.guidanceScale || 4.5, // 官方默认 4.5
+ enable_safety_checker: true,
+ output_format: "png",
},
logs: true,
onQueueUpdate: (update) => {
@@ -241,16 +189,18 @@ export async function POST(request: NextRequest) {
},
});
- // 8. 处理结果
- const outputImage = (result as { images?: Array<{ url?: string }> }).images?.[0];
+ // 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);
- // 9. 下载图片并上传到我们的 S3
+ // 7. 下载图片并上传到我们的 S3
const imageResponse = await fetch(outputImageUrl);
if (!imageResponse.ok) {
throw new Error(`下载输出图片失败: ${imageResponse.status}`);
@@ -261,33 +211,57 @@ export async function POST(request: NextRequest) {
const s3Result = await uploadBase64ToS3(imageBase64Result, "angle-edit");
console.log("[Angle-Edit] 已上传到S3:", s3Result.original);
- // 10. 保存到数据库
+ // 8. 保存到数据库
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",
+ prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`,
+ modelId: "qwen-image-edit-2511-multiple-angles",
createdAt: new Date(),
});
console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒");
- // 11. 返回结果
+ // 9. 返回结果
return NextResponse.json({
success: true,
image_url: s3Result.original,
thumbnail_url: s3Result.thumbnail,
image_base64: imageBase64Result,
processing_time: processingTime,
- preset: preset,
- angles: angleParams,
+ 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 : "角度编辑处理失败",
diff --git a/components/angle-edit-dialog.tsx b/components/angle-edit-dialog.tsx
index ff791a5..81505bf 100644
--- a/components/angle-edit-dialog.tsx
+++ b/components/angle-edit-dialog.tsx
@@ -23,40 +23,15 @@ interface AngleEditDialogProps {
onComplete?: (resultUrl: string) => void;
}
-// 根据角度值生成 API 所需的字符串参数
-function getAzimuthString(azimuth: number): string {
- const hAngle = azimuth % 360;
- if (hAngle < 22.5 || hAngle >= 337.5) return "front";
- if (hAngle < 67.5) return "front-right";
- if (hAngle < 112.5) return "right";
- if (hAngle < 157.5) return "back-right";
- if (hAngle < 202.5) return "back";
- if (hAngle < 247.5) return "back-left";
- if (hAngle < 292.5) return "left";
- return "front-left";
-}
-
-function getElevationString(elevation: number): string {
- if (elevation < -15) return "low";
- if (elevation < 15) return "eye-level";
- if (elevation < 45) return "elevated";
- return "high";
-}
-
-function getDistanceString(distance: number): string {
- if (distance < 3) return "wide";
- if (distance < 7) return "medium";
- return "close";
-}
-
export function AngleEditDialog({
open,
onOpenChange,
imageUrl,
+ onComplete,
}: AngleEditDialogProps) {
const [isProcessing, setIsProcessing] = useState(false);
const [resultUrl, setResultUrl] = useState(null);
- const [loraScale, setLoraScale] = useState(0.9);
+ const [loraScale, setLoraScale] = useState(1.0); // 官方默认值是 1
const [cameraState, setCameraState] = useState({
azimuth: 45,
elevation: 0,
@@ -97,14 +72,15 @@ export function AngleEditDialog({
setActiveTab("result"); // 切换到结果 Tab 显示加载状态
try {
+ // 使用官方 API 的数值参数格式
const response = await fetch("/api/angle-edit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
image_url: imageUrl,
- azimuth: getAzimuthString(cameraState.azimuth),
- elevation: getElevationString(cameraState.elevation),
- distance: getDistanceString(cameraState.distance),
+ horizontal_angle: cameraState.azimuth, // 0-360 度
+ vertical_angle: cameraState.elevation, // -30 到 90 度
+ zoom: cameraState.distance, // 0-10
lora_scale: loraScale,
}),
});
@@ -116,7 +92,7 @@ export function AngleEditDialog({
const data = await response.json();
setResultUrl(data.image_url);
- toast.success("视角生成成功!");
+ toast.success("视角生成成功,已保存到相册!");
} catch (error) {
console.error("角度编辑失败:", error);
toast.error(error instanceof Error ? error.message : "生成失败,请重试");
@@ -150,11 +126,15 @@ export function AngleEditDialog({
// 使用此图片(直接发送到聊天)
const handleUseImage = useCallback(() => {
if (resultUrl) {
+ // 如果提供了 onComplete 回调,调用它
+ if (onComplete) {
+ onComplete(resultUrl);
+ }
// 关闭对话框,图片已经通过 API 保存到数据库
onOpenChange(false);
toast.success("图片已添加到对话");
}
- }, [resultUrl, onOpenChange]);
+ }, [resultUrl, onOpenChange, onComplete]);
// 重置
const handleReset = useCallback(() => {
@@ -179,72 +159,73 @@ export function AngleEditDialog({
3D 视角编辑器