feat(scene-fusion): preprocess product with rmbg before fusion
This commit is contained in:
parent
d2070d93e1
commit
4ac636f8d1
@ -829,6 +829,7 @@ export function AdminSettingsClient() {
|
|||||||
// 批量处理配置状态
|
// 批量处理配置状态
|
||||||
const [sceneFusionConfig, setSceneFusionConfig] = useState({
|
const [sceneFusionConfig, setSceneFusionConfig] = useState({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
preRmbgEnabled: true,
|
||||||
defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
||||||
maxScenes: 10,
|
maxScenes: 10,
|
||||||
modelId: "", // 首选生图模型ID(多样性模式下优先尝试)
|
modelId: "", // 首选生图模型ID(多样性模式下优先尝试)
|
||||||
@ -1020,12 +1021,14 @@ export function AdminSettingsClient() {
|
|||||||
if (sceneFusionConfigData?.setting?.value) {
|
if (sceneFusionConfigData?.setting?.value) {
|
||||||
const config = sceneFusionConfigData.setting.value as {
|
const config = sceneFusionConfigData.setting.value as {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
preRmbgEnabled?: boolean;
|
||||||
defaultPrompt?: string;
|
defaultPrompt?: string;
|
||||||
maxScenes?: number;
|
maxScenes?: number;
|
||||||
modelId?: string;
|
modelId?: string;
|
||||||
};
|
};
|
||||||
setSceneFusionConfig({
|
setSceneFusionConfig({
|
||||||
enabled: config.enabled ?? true,
|
enabled: config.enabled ?? true,
|
||||||
|
preRmbgEnabled: config.preRmbgEnabled ?? true,
|
||||||
defaultPrompt: config.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
defaultPrompt: config.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
||||||
maxScenes: Math.min(config.maxScenes ?? 10, 10),
|
maxScenes: Math.min(config.maxScenes ?? 10, 10),
|
||||||
modelId: config.modelId ?? "",
|
modelId: config.modelId ?? "",
|
||||||
@ -2117,6 +2120,7 @@ export function AdminSettingsClient() {
|
|||||||
key: "scene_fusion_config",
|
key: "scene_fusion_config",
|
||||||
value: {
|
value: {
|
||||||
enabled: sceneFusionConfig.enabled,
|
enabled: sceneFusionConfig.enabled,
|
||||||
|
preRmbgEnabled: sceneFusionConfig.preRmbgEnabled,
|
||||||
defaultPrompt: sceneFusionConfig.defaultPrompt.trim(),
|
defaultPrompt: sceneFusionConfig.defaultPrompt.trim(),
|
||||||
maxScenes: Math.max(1, Math.min(10, sceneFusionConfig.maxScenes || 10)),
|
maxScenes: Math.max(1, Math.min(10, sceneFusionConfig.maxScenes || 10)),
|
||||||
modelId: sceneFusionConfig.modelId,
|
modelId: sceneFusionConfig.modelId,
|
||||||
@ -8661,6 +8665,27 @@ export function AdminSettingsClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label className="text-base">场景裂变前自动抠图</Label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
先对产品图做抠图预处理,再进行裂变融合;抠图失败将自动降级为原图继续生成
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={sceneFusionConfig.preRmbgEnabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setSceneFusionConfig({ ...sceneFusionConfig, preRmbgEnabled: checked })
|
||||||
|
}
|
||||||
|
disabled={!sceneFusionConfig.enabled}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground min-w-[3rem]">
|
||||||
|
{sceneFusionConfig.preRmbgEnabled ? "已开启" : "已关闭"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 默认提示词 */}
|
{/* 默认提示词 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="sceneFusionPrompt">默认提示词 *</Label>
|
<Label htmlFor="sceneFusionPrompt">默认提示词 *</Label>
|
||||||
|
|||||||
@ -501,12 +501,14 @@ export async function POST(request: NextRequest) {
|
|||||||
let prompt = "";
|
let prompt = "";
|
||||||
let chatId: string | null = null;
|
let chatId: string | null = null;
|
||||||
let messageId: string | null = null;
|
let messageId: string | null = null;
|
||||||
|
let skipGallerySave = false;
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
if (contentType.includes("application/json")) {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
prompt = body.prompt || "抠图处理";
|
prompt = body.prompt || "抠图处理";
|
||||||
chatId = body.chatId || null;
|
chatId = body.chatId || null;
|
||||||
messageId = body.messageId || null;
|
messageId = body.messageId || null;
|
||||||
|
skipGallerySave = body.skipGallerySave === true || body.skip_gallery_save === 1;
|
||||||
const imageUrl = body.imageUrl || body.image_url;
|
const imageUrl = body.imageUrl || body.image_url;
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
@ -644,21 +646,25 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 保存图片记录到数据库
|
// 5. 保存图片记录到数据库(可选)
|
||||||
try {
|
if (!skipGallerySave) {
|
||||||
const [savedImage] = await db.insert(generatedImage).values({
|
try {
|
||||||
userId,
|
const [savedImage] = await db.insert(generatedImage).values({
|
||||||
chatId: chatId || null,
|
userId,
|
||||||
messageId: messageId || null,
|
chatId: chatId || null,
|
||||||
thumbnailUrl,
|
messageId: messageId || null,
|
||||||
originalUrl,
|
thumbnailUrl,
|
||||||
imageSize: "2K",
|
originalUrl,
|
||||||
prompt: prompt || "抠图处理",
|
imageSize: "2K",
|
||||||
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
prompt: prompt || "抠图处理",
|
||||||
}).returning();
|
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
||||||
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
}).returning();
|
||||||
} catch (dbError) {
|
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
||||||
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
|
} catch (dbError) {
|
||||||
|
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("[RMBG] Skip gallery save by request flag");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 返回结果
|
// 6. 返回结果
|
||||||
|
|||||||
@ -31,10 +31,12 @@ export async function GET() {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
defaultPrompt?: string;
|
defaultPrompt?: string;
|
||||||
maxScenes?: number;
|
maxScenes?: number;
|
||||||
|
preRmbgEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
enabled: config.enabled ?? true,
|
enabled: config.enabled ?? true,
|
||||||
|
preRmbgEnabled: config.preRmbgEnabled ?? true,
|
||||||
defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT,
|
defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT,
|
||||||
maxScenes: Math.min(config.maxScenes ?? SCENE_FUSION_MAX_ITEMS, SCENE_FUSION_MAX_ITEMS),
|
maxScenes: Math.min(config.maxScenes ?? SCENE_FUSION_MAX_ITEMS, SCENE_FUSION_MAX_ITEMS),
|
||||||
mode: "diversity",
|
mode: "diversity",
|
||||||
@ -44,6 +46,7 @@ export async function GET() {
|
|||||||
// 返回默认配置
|
// 返回默认配置
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
preRmbgEnabled: true,
|
||||||
defaultPrompt: FALLBACK_PROMPT,
|
defaultPrompt: FALLBACK_PROMPT,
|
||||||
maxScenes: SCENE_FUSION_MAX_ITEMS,
|
maxScenes: SCENE_FUSION_MAX_ITEMS,
|
||||||
mode: "diversity",
|
mode: "diversity",
|
||||||
|
|||||||
@ -427,12 +427,14 @@ async function getSceneFusionConfig() {
|
|||||||
defaultPrompt?: string;
|
defaultPrompt?: string;
|
||||||
maxScenes?: number;
|
maxScenes?: number;
|
||||||
modelId?: string;
|
modelId?: string;
|
||||||
|
preRmbgEnabled?: boolean;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
enabled: config.enabled ?? true,
|
enabled: config.enabled ?? true,
|
||||||
defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT,
|
defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT,
|
||||||
maxScenes: Math.min(config.maxScenes ?? SCENE_FUSION_MAX_ITEMS, SCENE_FUSION_MAX_ITEMS),
|
maxScenes: Math.min(config.maxScenes ?? SCENE_FUSION_MAX_ITEMS, SCENE_FUSION_MAX_ITEMS),
|
||||||
modelId: config.modelId || "",
|
modelId: config.modelId || "",
|
||||||
|
preRmbgEnabled: config.preRmbgEnabled ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -444,9 +446,98 @@ async function getSceneFusionConfig() {
|
|||||||
defaultPrompt: FALLBACK_PROMPT,
|
defaultPrompt: FALLBACK_PROMPT,
|
||||||
maxScenes: SCENE_FUSION_MAX_ITEMS,
|
maxScenes: SCENE_FUSION_MAX_ITEMS,
|
||||||
modelId: "",
|
modelId: "",
|
||||||
|
preRmbgEnabled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function preprocessProductImageWithRmbg(
|
||||||
|
request: Request,
|
||||||
|
requestId: string,
|
||||||
|
productImageUrl: string,
|
||||||
|
preRmbgEnabled: boolean
|
||||||
|
): Promise<{
|
||||||
|
productImageUrl: string;
|
||||||
|
usedPreRmbg: boolean;
|
||||||
|
provider?: string;
|
||||||
|
reason?: string;
|
||||||
|
}> {
|
||||||
|
if (!preRmbgEnabled) {
|
||||||
|
return {
|
||||||
|
productImageUrl,
|
||||||
|
usedPreRmbg: false,
|
||||||
|
reason: "disabled_by_config",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = request.headers.get("cookie");
|
||||||
|
if (!cookie) {
|
||||||
|
return {
|
||||||
|
productImageUrl,
|
||||||
|
usedPreRmbg: false,
|
||||||
|
reason: "missing_cookie",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rmbgUrl = new URL("/api/rmbg", request.url).toString();
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 90_000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[SceneFusion][${requestId}] Pre-RMBG start: ${productImageUrl.substring(0, 120)}...`);
|
||||||
|
const response = await fetch(rmbgUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
cookie,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
imageUrl: productImageUrl,
|
||||||
|
prompt: "场景裂变预处理-抠图",
|
||||||
|
skipGallerySave: true,
|
||||||
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => null) as Record<string, unknown> | null;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`[SceneFusion][${requestId}] Pre-RMBG fallback: request_failed`, {
|
||||||
|
status: response.status,
|
||||||
|
detail: payload?.detail || payload?.error || "unknown",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
productImageUrl,
|
||||||
|
usedPreRmbg: false,
|
||||||
|
reason: "request_failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedUrl = String(payload?.original_url || payload?.thumbnail_url || "").trim();
|
||||||
|
if (!cleanedUrl || cleanedUrl === productImageUrl) {
|
||||||
|
return {
|
||||||
|
productImageUrl,
|
||||||
|
usedPreRmbg: false,
|
||||||
|
reason: cleanedUrl ? "same_as_source" : "empty_result",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
productImageUrl: cleanedUrl,
|
||||||
|
usedPreRmbg: true,
|
||||||
|
provider: String(payload?.provider || "unknown"),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[SceneFusion][${requestId}] Pre-RMBG fallback: exception`, error);
|
||||||
|
return {
|
||||||
|
productImageUrl,
|
||||||
|
usedPreRmbg: false,
|
||||||
|
reason: error instanceof Error ? error.message : "exception",
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建缩略图
|
* 创建缩略图
|
||||||
*/
|
*/
|
||||||
@ -1240,6 +1331,16 @@ export async function POST(request: Request) {
|
|||||||
if (productImage.startsWith("data:")) {
|
if (productImage.startsWith("data:")) {
|
||||||
productImageUrl = await uploadBase64ToS3(productImage, "scene-fusion-product");
|
productImageUrl = await uploadBase64ToS3(productImage, "scene-fusion-product");
|
||||||
}
|
}
|
||||||
|
const preprocessedProduct = await preprocessProductImageWithRmbg(
|
||||||
|
request,
|
||||||
|
requestId,
|
||||||
|
productImageUrl,
|
||||||
|
config.preRmbgEnabled
|
||||||
|
);
|
||||||
|
const fusionProductImageUrl = preprocessedProduct.productImageUrl;
|
||||||
|
console.log(
|
||||||
|
`[SceneFusion][${requestId}] Pre-RMBG result: used=${preprocessedProduct.usedPreRmbg}, provider=${preprocessedProduct.provider || "-"}, reason=${preprocessedProduct.reason || "-"}`
|
||||||
|
);
|
||||||
|
|
||||||
const processedScenes = await Promise.all(
|
const processedScenes = await Promise.all(
|
||||||
scenes.map(async (scene) => {
|
scenes.map(async (scene) => {
|
||||||
@ -1256,7 +1357,7 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const enriched = await enrichSceneLabelsWithAgent({
|
const enriched = await enrichSceneLabelsWithAgent({
|
||||||
requestId,
|
requestId,
|
||||||
productImageUrl,
|
productImageUrl: fusionProductImageUrl,
|
||||||
productName,
|
productName,
|
||||||
scenes: processedScenes,
|
scenes: processedScenes,
|
||||||
});
|
});
|
||||||
@ -1340,7 +1441,7 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
console.log(`[SceneFusion] Scene ID: ${scene.id}`);
|
console.log(`[SceneFusion] Scene ID: ${scene.id}`);
|
||||||
console.log(
|
console.log(
|
||||||
`[SceneFusion] Product Image URL: ${productImageUrl.substring(0, 80)}...`
|
`[SceneFusion] Product Image URL: ${fusionProductImageUrl.substring(0, 80)}...`
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`
|
`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`
|
||||||
@ -1354,7 +1455,7 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { result, candidate, attempts } = await processSceneWithDiversity({
|
const { result, candidate, attempts } = await processSceneWithDiversity({
|
||||||
productImageUrl,
|
productImageUrl: fusionProductImageUrl,
|
||||||
sceneImageUrl: resolvedScene.imageUrl,
|
sceneImageUrl: resolvedScene.imageUrl,
|
||||||
productName: resolvedProductName,
|
productName: resolvedProductName,
|
||||||
targetName: resolvedScene.targetName,
|
targetName: resolvedScene.targetName,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user