diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index 464e867a1..703991a8a 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -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() {
+
+
+
+
+ 先对产品图做抠图预处理,再进行裂变融合;抠图失败将自动降级为原图继续生成
+
+
+
+
+ setSceneFusionConfig({ ...sceneFusionConfig, preRmbgEnabled: checked })
+ }
+ disabled={!sceneFusionConfig.enabled}
+ />
+
+ {sceneFusionConfig.preRmbgEnabled ? "已开启" : "已关闭"}
+
+
+
+
{/* 默认提示词 */}
diff --git a/app/api/rmbg/route.ts b/app/api/rmbg/route.ts
index c0312251c..c7eb256fc 100644
--- a/app/api/rmbg/route.ts
+++ b/app/api/rmbg/route.ts
@@ -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. 返回结果
diff --git a/app/api/scene-fusion/config/route.ts b/app/api/scene-fusion/config/route.ts
index 9884e8ee2..3d0be993f 100644
--- a/app/api/scene-fusion/config/route.ts
+++ b/app/api/scene-fusion/config/route.ts
@@ -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",
diff --git a/app/api/scene-fusion/route.ts b/app/api/scene-fusion/route.ts
index 1d772ae5a..87e0da0e5 100644
--- a/app/api/scene-fusion/route.ts
+++ b/app/api/scene-fusion/route.ts
@@ -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 | 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,