diff --git a/app/api/scene-fusion/route.ts b/app/api/scene-fusion/route.ts index b03f92e..990c5b8 100644 --- a/app/api/scene-fusion/route.ts +++ b/app/api/scene-fusion/route.ts @@ -17,9 +17,11 @@ import sharp from "sharp"; // 请求体 Schema const sceneFusionRequestSchema = z.object({ productImage: z.string().min(1, "产品图不能为空"), + productName: z.string().optional(), // 产品名称(如"沙发") scenes: z.array(z.object({ id: z.string(), image: z.string().min(1, "场景图不能为空"), + targetName: z.string().optional(), // 背景中需要被替换的物体名称(如"椅子") customPrompt: z.string().optional(), })).min(1, "至少需要一个场景图").max(20, "最多支持20个场景图"), }); @@ -220,22 +222,56 @@ async function callAlibabaImageAPI( return generatedImageUrl; } +/** + * 构建智能提示词 + * @param defaultPrompt - 默认提示词模板 + * @param productName - 产品名称(如"沙发") + * @param targetName - 背景中需要被替换的物体名称(如"椅子") + * @param customPrompt - 用户补充的关键词 + */ +function buildPrompt( + defaultPrompt: string, + productName?: string, + targetName?: string, + customPrompt?: string +): string { + let prompt = defaultPrompt; + + // 如果用户提供了具体的产品名称和被替换物体名称,构建更精确的提示词 + if (productName && targetName) { + prompt = `将图1中的"${productName}"替换掉图2场景中的"${targetName}",保持"${productName}"的外观、颜色和细节不变,自然融合到图2的环境中。`; + } else if (productName) { + prompt = `将图1中的"${productName}"融合到图2的场景中,保持"${productName}"的外观、颜色和细节不变,自然融合到图2的环境中。`; + } else if (targetName) { + prompt = `将图1的产品替换掉图2场景中的"${targetName}",保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。`; + } + + // 添加用户补充的关键词 + if (customPrompt) { + prompt = `${prompt} ${customPrompt}`; + } + + return prompt; +} + /** * 处理单个场景 */ async function processScene( productImageUrl: string, sceneImageUrl: string, + productName: string | undefined, + targetName: string | undefined, customPrompt: string | undefined, defaultPrompt: string, baseURL: string, apiKey: string, modelId: string -): Promise<{ thumbnail: string; original: string }> { - // 组合提示词 - const prompt = customPrompt - ? `${defaultPrompt} ${customPrompt}` - : defaultPrompt; +): Promise<{ thumbnail: string; original: string; prompt: string }> { + // 构建智能提示词 + const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt); + + console.log(`[SceneFusion] Built prompt: ${prompt}`); // 调用阿里生图 API const generatedImageUrl = await callAlibabaImageAPI( @@ -250,7 +286,7 @@ async function processScene( // 下载并上传到 S3 const uploaded = await downloadAndUploadToS3(generatedImageUrl, "scene-fusion"); - return uploaded; + return { ...uploaded, prompt }; } export async function POST(request: Request) { @@ -285,7 +321,7 @@ export async function POST(request: Request) { ); } - const { productImage, scenes } = parseResult.data; + const { productImage, productName, scenes } = parseResult.data; // 检查场景数量限制 if (scenes.length > config.maxScenes) { @@ -296,7 +332,8 @@ export async function POST(request: Request) { } console.log(`[SceneFusion] Processing ${scenes.length} scenes for user ${session.user.id}`); - console.log(`[SceneFusion] Using prompt: ${config.defaultPrompt.substring(0, 50)}...`); + console.log(`[SceneFusion] Product name: ${productName || '(not specified)'}`); + console.log(`[SceneFusion] Using default prompt: ${config.defaultPrompt.substring(0, 50)}...`); // 4. 获取阿里生图模型配置 const allModels = await getEnabledAiModelsWithProvider(); @@ -343,6 +380,7 @@ export async function POST(request: Request) { console.log(`[SceneFusion] Scene ID: ${scene.id}`); console.log(`[SceneFusion] Product Image URL: ${productImageUrl.substring(0, 80)}...`); console.log(`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`); + console.log(`[SceneFusion] Target Name: ${scene.targetName || '(not specified)'}`); console.log(`[SceneFusion] Custom Prompt: ${scene.customPrompt || '(none)'}`); // 如果不是第一张图,等待5秒 @@ -355,6 +393,8 @@ export async function POST(request: Request) { const result = await processScene( productImageUrl, scene.imageUrl, + productName, + scene.targetName, scene.customPrompt, config.defaultPrompt, provider.baseUrl, @@ -371,9 +411,7 @@ export async function POST(request: Request) { thumbnailUrl: result.thumbnail, originalUrl: result.original, imageSize: "2K", - prompt: scene.customPrompt - ? `[场景裂变] ${config.defaultPrompt} ${scene.customPrompt}` - : `[场景裂变] ${config.defaultPrompt}`, + prompt: `[场景裂变] ${result.prompt}`, modelId: model.modelId, }); console.log(`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`); diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index 3aca289..9ed3d80 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -28,6 +28,7 @@ export interface SceneItem { id: string; image: string; // 场景图 URL 或 Base64 customPrompt: string; // 该场景的补充关键词 + targetName: string; // 背景中需要被替换的物体名称 status: 'pending' | 'processing' | 'completed' | 'failed'; result?: string; // 生成结果缩略图 URL originalUrl?: string; // 生成结果原图 URL(用于下载) @@ -56,6 +57,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { // 状态管理 const [productImage, setProductImage] = useState(null); + const [productName, setProductName] = useState(""); // 产品名称(如"沙发") const [scenes, setScenes] = useState([]); const [isProcessing, setIsProcessing] = useState(false); @@ -104,6 +106,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { id: Math.random().toString(36).substring(7), image: e.target?.result as string, customPrompt: "", + targetName: "", // 背景中需要被替换的物体名称 status: 'pending' }; setScenes(prev => [...prev, newScene]); @@ -127,6 +130,13 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { )); }; + // 更新场景中需要被替换的物体名称 + const updateSceneTargetName = (id: string, targetName: string) => { + setScenes(prev => prev.map(scene => + scene.id === id ? { ...scene, targetName } : scene + )); + }; + // 开始裂变 const startFusion = async () => { if (config?.enabled === false) { @@ -161,9 +171,11 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { }, body: JSON.stringify({ productImage, + productName: productName || undefined, // 产品名称 scenes: scenesToProcess.map(s => ({ id: s.id, image: s.image, + targetName: s.targetName || undefined, // 背景中需要被替换的物体名称 customPrompt: s.customPrompt || undefined, })), }), @@ -235,9 +247,11 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { }, body: JSON.stringify({ productImage, + productName: productName || undefined, scenes: [{ id: scene.id, image: scene.image, + targetName: scene.targetName || undefined, customPrompt: scene.customPrompt || undefined, }], }), @@ -370,6 +384,20 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { )} + + {/* 产品名称输入 */} + {productImage && ( +
+ setProductName(e.target.value)} + className="h-9 text-sm" + disabled={isProcessing} + /> +

输入产品名称可以让 AI 更精准理解

+
+ )} {/* 2. 场景图列表 */} @@ -417,7 +445,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { {`场景 - {/* 关键词输入 */} + {/* 输入区域 */}
场景 {index + 1} @@ -433,10 +461,17 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) { )}
updateSceneTargetName(scene.id, e.target.value)} + className="h-7 text-xs" + disabled={isProcessing} + /> + updateScenePrompt(scene.id, e.target.value)} - className="h-8 text-xs" + className="h-7 text-xs" disabled={isProcessing} />