feat: add product name and target name inputs for more precise scene fusion prompts

This commit is contained in:
Your Name 2026-01-18 16:23:16 +08:00
parent 9f25cc3708
commit fbf2ecaf87
2 changed files with 87 additions and 14 deletions

View File

@ -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)}...`);

View File

@ -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<string | null>(null);
const [productName, setProductName] = useState<string>(""); // 产品名称(如"沙发")
const [scenes, setScenes] = useState<SceneItem[]>([]);
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) {
</div>
)}
</div>
{/* 产品名称输入 */}
{productImage && (
<div className="mt-2">
<Input
placeholder="产品名称(如:沙发、椅子、花瓶)"
value={productName}
onChange={(e) => setProductName(e.target.value)}
className="h-9 text-sm"
disabled={isProcessing}
/>
<p className="text-xs text-muted-foreground mt-1">输入产品名称可以让 AI 更精准理解</p>
</div>
)}
</div>
{/* 2. 场景图列表 */}
@ -417,7 +445,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
<img src={scene.image} alt={`场景 ${index + 1}`} className="w-full h-full object-cover" />
</div>
{/* 关键词输入 */}
{/* 输入区域 */}
<div className="flex-1 space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">场景 {index + 1}</span>
@ -433,10 +461,17 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
)}
</div>
<Input
placeholder="补充关键词:如'放在木桌上'"
placeholder="被替换物体(如:椅子、桌子)"
value={scene.targetName}
onChange={(e) => updateSceneTargetName(scene.id, e.target.value)}
className="h-7 text-xs"
disabled={isProcessing}
/>
<Input
placeholder="补充关键词(可选)"
value={scene.customPrompt}
onChange={(e) => updateScenePrompt(scene.id, e.target.value)}
className="h-8 text-xs"
className="h-7 text-xs"
disabled={isProcessing}
/>
</div>