diff --git a/app/api/scene-fusion/route.ts b/app/api/scene-fusion/route.ts index 8d9e23db7..3490a2070 100644 --- a/app/api/scene-fusion/route.ts +++ b/app/api/scene-fusion/route.ts @@ -18,6 +18,8 @@ import sharp from "sharp"; const sceneFusionRequestSchema = z.object({ productImage: z.string().min(1, "产品图不能为空"), productName: z.string().optional(), // 产品名称(如"沙发") + aspectRatio: z.string().optional(), // 图片比例(如"1:1", "4:3", "auto") + imageSize: z.string().optional(), // 图片分辨率(如"1K", "2K", "4K") scenes: z.array(z.object({ id: z.string(), image: z.string().min(1, "场景图不能为空"), @@ -144,6 +146,54 @@ async function downloadAndUploadToS3( } } +/** + * 根据 aspectRatio 和 imageSize 计算实际尺寸 + * @param aspectRatio - 宽高比,如 "1:1", "4:3", "16:9", "auto" + * @param imageSize - 分辨率等级,如 "1K", "2K", "4K" + * @returns 尺寸字符串,如 "1024*1024",或 undefined(auto 模式) + */ +function calculateSize(aspectRatio: string, imageSize: string): string | undefined { + // auto 模式:不设置尺寸,让 API 根据输入图片自动决定 + if (aspectRatio === "auto") { + return undefined; + } + + // 根据分辨率等级确定基准像素 + const baseSizeMap: Record = { + "1K": 1024, + "2K": 1536, + "4K": 2048, + }; + const baseSize = baseSizeMap[imageSize] || 1536; + + // 根据比例计算宽高 + const ratioMap: Record = { + "1:1": [1, 1], + "4:3": [4, 3], + "3:4": [3, 4], + "16:9": [16, 9], + "9:16": [9, 16], + }; + + const ratio = ratioMap[aspectRatio]; + if (!ratio) { + return undefined; + } + + const [w, h] = ratio; + // 保持总像素接近 baseSize * baseSize,同时维持比例 + const scale = Math.sqrt((baseSize * baseSize) / (w * h)); + let width = Math.round(w * scale); + let height = Math.round(h * scale); + + // 确保宽高在 512-2048 范围内 + const clamp = (v: number) => Math.max(512, Math.min(2048, v)); + width = clamp(width); + height = clamp(height); + + return `${width}*${height}`; +} + /** * 调用阿里生图 API */ @@ -153,7 +203,8 @@ async function callAlibabaImageAPI( modelId: string, productImageUrl: string, sceneImageUrl: string, - prompt: string + prompt: string, + size?: string ): Promise { // 构建消息内容:产品图 + 场景图 + 提示词 const content = [ @@ -174,6 +225,7 @@ async function callAlibabaImageAPI( }, parameters: { n: 1, + ...(size && { size }), prompt_extend: true, watermark: false, }, @@ -183,6 +235,7 @@ async function callAlibabaImageAPI( - Index: Scene ${sceneImageUrl.substring(sceneImageUrl.length-15)} - Product URL: ${productImageUrl} - Scene URL: ${sceneImageUrl} + - Size: ${size || 'auto'} - Prompt: ${prompt} `); @@ -267,7 +320,8 @@ async function processScene( defaultPrompt: string, baseURL: string, apiKey: string, - modelId: string + modelId: string, + size?: string ): Promise<{ thumbnail: string; original: string; prompt: string }> { // 构建智能提示词 const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt); @@ -281,7 +335,8 @@ async function processScene( modelId, productImageUrl, sceneImageUrl, - prompt + prompt, + size ); // 下载并上传到 S3 @@ -322,7 +377,11 @@ export async function POST(request: Request) { ); } - const { productImage, productName, scenes } = parseResult.data; + const { productImage, productName, aspectRatio, imageSize, scenes } = parseResult.data; + + // 计算输出尺寸 + const size = calculateSize(aspectRatio || "auto", imageSize || "2K"); + console.log(`[SceneFusion] Output settings: aspectRatio=${aspectRatio || 'auto'}, imageSize=${imageSize || '2K'}, calculated size=${size || 'auto'}`); // 检查场景数量限制 if (scenes.length > config.maxScenes) { @@ -400,9 +459,13 @@ export async function POST(request: Request) { config.defaultPrompt, provider.baseUrl, provider.apiKey, - model.modelId + model.modelId, + size ); + // 将分辨率等级用于数据库存储 + const dbImageSize = imageSize || "2K"; + // 保存到数据库(chatId 和 messageId 设为 null,因为场景裂变不关联聊天) try { await db.insert(generatedImage).values({ @@ -411,7 +474,7 @@ export async function POST(request: Request) { messageId: null, // 场景裂变不关联消息 thumbnailUrl: result.thumbnail, originalUrl: result.original, - imageSize: "2K", + imageSize: dbImageSize, prompt: `[场景裂变] ${result.prompt}`, modelId: model.modelId, }); diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index 01a2f195a..3a5a7edde 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -55,11 +55,30 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { const defaultPrompt = config?.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。"; const maxScenes = config?.maxScenes || 10; + // 比例选项 + const aspectRatioOptions = [ + { label: "自动", value: "auto" }, + { label: "1:1", value: "1:1" }, + { label: "4:3", value: "4:3" }, + { label: "3:4", value: "3:4" }, + { label: "16:9", value: "16:9" }, + { label: "9:16", value: "9:16" }, + ]; + + // 分辨率选项 + const imageSizeOptions = [ + { label: "标准 (1K)", value: "1K" }, + { label: "高清 (2K)", value: "2K" }, + { label: "超清 (4K)", value: "4K" }, + ]; + // 状态管理 const [productImage, setProductImage] = useState(null); const [productName, setProductName] = useState(""); // 产品名称(如"沙发") const [scenes, setScenes] = useState([]); const [isProcessing, setIsProcessing] = useState(false); + const [aspectRatio, setAspectRatio] = useState("auto"); // 图片比例 + const [imageSize, setImageSize] = useState("2K"); // 图片分辨率 // 文件上传 Input 引用 const productInputRef = useRef(null); @@ -172,6 +191,8 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { body: JSON.stringify({ productImage, productName: productName || undefined, // 产品名称 + aspectRatio, // 图片比例 + imageSize, // 图片分辨率 scenes: scenesToProcess.map(s => ({ id: s.id, image: s.image, @@ -248,6 +269,8 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { body: JSON.stringify({ productImage, productName: productName || undefined, + aspectRatio, // 图片比例 + imageSize, // 图片分辨率 scenes: [{ id: scene.id, image: scene.image, @@ -400,6 +423,55 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { )} + {/* 1.5 图片尺寸设置 */} +
+ + + {/* 比例选择 */} +
+ +
+ {aspectRatioOptions.map((option) => ( + + ))} +
+
+ + {/* 分辨率选择 */} +
+ +
+ {imageSizeOptions.map((option) => ( + + ))} +
+
+
+ {/* 2. 场景图列表 */}