feat: add product name and target name inputs for more precise scene fusion prompts
This commit is contained in:
parent
9f25cc3708
commit
fbf2ecaf87
@ -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)}...`);
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user