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({
|
||||
enabled: true,
|
||||
preRmbgEnabled: true,
|
||||
defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
||||
maxScenes: 10,
|
||||
modelId: "", // 首选生图模型ID(多样性模式下优先尝试)
|
||||
@ -1020,12 +1021,14 @@ export function AdminSettingsClient() {
|
||||
if (sceneFusionConfigData?.setting?.value) {
|
||||
const config = sceneFusionConfigData.setting.value as {
|
||||
enabled?: boolean;
|
||||
preRmbgEnabled?: boolean;
|
||||
defaultPrompt?: string;
|
||||
maxScenes?: number;
|
||||
modelId?: string;
|
||||
};
|
||||
setSceneFusionConfig({
|
||||
enabled: config.enabled ?? true,
|
||||
preRmbgEnabled: config.preRmbgEnabled ?? true,
|
||||
defaultPrompt: config.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
||||
maxScenes: Math.min(config.maxScenes ?? 10, 10),
|
||||
modelId: config.modelId ?? "",
|
||||
@ -2117,6 +2120,7 @@ export function AdminSettingsClient() {
|
||||
key: "scene_fusion_config",
|
||||
value: {
|
||||
enabled: sceneFusionConfig.enabled,
|
||||
preRmbgEnabled: sceneFusionConfig.preRmbgEnabled,
|
||||
defaultPrompt: sceneFusionConfig.defaultPrompt.trim(),
|
||||
maxScenes: Math.max(1, Math.min(10, sceneFusionConfig.maxScenes || 10)),
|
||||
modelId: sceneFusionConfig.modelId,
|
||||
@ -8661,6 +8665,27 @@ export function AdminSettingsClient() {
|
||||
</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">
|
||||
<Label htmlFor="sceneFusionPrompt">默认提示词 *</Label>
|
||||
|
||||
@ -501,12 +501,14 @@ export async function POST(request: NextRequest) {
|
||||
let prompt = "";
|
||||
let chatId: string | null = null;
|
||||
let messageId: string | null = null;
|
||||
let skipGallerySave = false;
|
||||
|
||||
if (contentType.includes("application/json")) {
|
||||
const body = await request.json();
|
||||
prompt = body.prompt || "抠图处理";
|
||||
chatId = body.chatId || null;
|
||||
messageId = body.messageId || null;
|
||||
skipGallerySave = body.skipGallerySave === true || body.skip_gallery_save === 1;
|
||||
const imageUrl = body.imageUrl || body.image_url;
|
||||
|
||||
if (imageUrl) {
|
||||
@ -644,21 +646,25 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 保存图片记录到数据库
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: "2K",
|
||||
prompt: prompt || "抠图处理",
|
||||
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
||||
}).returning();
|
||||
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
||||
} catch (dbError) {
|
||||
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
|
||||
// 5. 保存图片记录到数据库(可选)
|
||||
if (!skipGallerySave) {
|
||||
try {
|
||||
const [savedImage] = await db.insert(generatedImage).values({
|
||||
userId,
|
||||
chatId: chatId || null,
|
||||
messageId: messageId || null,
|
||||
thumbnailUrl,
|
||||
originalUrl,
|
||||
imageSize: "2K",
|
||||
prompt: prompt || "抠图处理",
|
||||
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
||||
}).returning();
|
||||
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
||||
} catch (dbError) {
|
||||
console.error("[RMBG] ❌ 保存到数据库失败:", dbError);
|
||||
}
|
||||
} else {
|
||||
console.log("[RMBG] Skip gallery save by request flag");
|
||||
}
|
||||
|
||||
// 6. 返回结果
|
||||
|
||||
@ -31,10 +31,12 @@ export async function GET() {
|
||||
enabled?: boolean;
|
||||
defaultPrompt?: string;
|
||||
maxScenes?: number;
|
||||
preRmbgEnabled?: boolean;
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
enabled: config.enabled ?? true,
|
||||
preRmbgEnabled: config.preRmbgEnabled ?? true,
|
||||
defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT,
|
||||
maxScenes: Math.min(config.maxScenes ?? SCENE_FUSION_MAX_ITEMS, SCENE_FUSION_MAX_ITEMS),
|
||||
mode: "diversity",
|
||||
@ -44,6 +46,7 @@ export async function GET() {
|
||||
// 返回默认配置
|
||||
return NextResponse.json({
|
||||
enabled: true,
|
||||
preRmbgEnabled: true,
|
||||
defaultPrompt: FALLBACK_PROMPT,
|
||||
maxScenes: SCENE_FUSION_MAX_ITEMS,
|
||||
mode: "diversity",
|
||||
|
||||
@ -427,12 +427,14 @@ async function getSceneFusionConfig() {
|
||||
defaultPrompt?: string;
|
||||
maxScenes?: number;
|
||||
modelId?: string;
|
||||
preRmbgEnabled?: boolean;
|
||||
};
|
||||
return {
|
||||
enabled: config.enabled ?? true,
|
||||
defaultPrompt: config.defaultPrompt || FALLBACK_PROMPT,
|
||||
maxScenes: Math.min(config.maxScenes ?? SCENE_FUSION_MAX_ITEMS, SCENE_FUSION_MAX_ITEMS),
|
||||
modelId: config.modelId || "",
|
||||
preRmbgEnabled: config.preRmbgEnabled ?? true,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
@ -444,9 +446,98 @@ async function getSceneFusionConfig() {
|
||||
defaultPrompt: FALLBACK_PROMPT,
|
||||
maxScenes: SCENE_FUSION_MAX_ITEMS,
|
||||
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:")) {
|
||||
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(
|
||||
scenes.map(async (scene) => {
|
||||
@ -1256,7 +1357,7 @@ export async function POST(request: Request) {
|
||||
|
||||
const enriched = await enrichSceneLabelsWithAgent({
|
||||
requestId,
|
||||
productImageUrl,
|
||||
productImageUrl: fusionProductImageUrl,
|
||||
productName,
|
||||
scenes: processedScenes,
|
||||
});
|
||||
@ -1340,7 +1441,7 @@ export async function POST(request: Request) {
|
||||
);
|
||||
console.log(`[SceneFusion] Scene ID: ${scene.id}`);
|
||||
console.log(
|
||||
`[SceneFusion] Product Image URL: ${productImageUrl.substring(0, 80)}...`
|
||||
`[SceneFusion] Product Image URL: ${fusionProductImageUrl.substring(0, 80)}...`
|
||||
);
|
||||
console.log(
|
||||
`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`
|
||||
@ -1354,7 +1455,7 @@ export async function POST(request: Request) {
|
||||
|
||||
try {
|
||||
const { result, candidate, attempts } = await processSceneWithDiversity({
|
||||
productImageUrl,
|
||||
productImageUrl: fusionProductImageUrl,
|
||||
sceneImageUrl: resolvedScene.imageUrl,
|
||||
productName: resolvedProductName,
|
||||
targetName: resolvedScene.targetName,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user