feat: rename scene fusion to detail replace, add auto-retry up to 10 times
This commit is contained in:
parent
0d69e8d9fa
commit
59bc3636e1
@ -408,7 +408,7 @@ export function AdminSettingsClient() {
|
||||
timeout: 120,
|
||||
});
|
||||
|
||||
// 场景裂变配置状态
|
||||
// 详情替换配置状态
|
||||
const [sceneFusionConfig, setSceneFusionConfig] = useState({
|
||||
enabled: true,
|
||||
defaultPrompt: "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。",
|
||||
@ -499,7 +499,7 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
}, [angleEditConfigData]);
|
||||
|
||||
// 加载场景裂变配置
|
||||
// 加载详情替换配置
|
||||
const { data: sceneFusionConfigData } = useSWR("/api/admin/settings?key=scene_fusion_config", fetcher);
|
||||
useEffect(() => {
|
||||
if (sceneFusionConfigData?.setting?.value) {
|
||||
@ -961,7 +961,7 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
};
|
||||
|
||||
// --- 场景裂变配置逻辑 ---
|
||||
// --- 详情替换配置逻辑 ---
|
||||
const handleSaveSceneFusionConfig = async () => {
|
||||
try {
|
||||
if (!sceneFusionConfig.defaultPrompt.trim()) {
|
||||
@ -984,9 +984,9 @@ export function AdminSettingsClient() {
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("保存失败");
|
||||
toast.success("场景裂变配置保存成功");
|
||||
toast.success("详情替换配置保存成功");
|
||||
} catch (error) {
|
||||
toast.error("保存场景裂变配置失败");
|
||||
toast.error("保存详情替换配置失败");
|
||||
}
|
||||
};
|
||||
|
||||
@ -3894,24 +3894,24 @@ export function AdminSettingsClient() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 场景裂变配置 */}
|
||||
{/* 详情替换配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
场景裂变配置
|
||||
详情替换配置
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
配置场景裂变功能的默认提示词和参数。场景裂变可将产品图批量融合到多个场景中。
|
||||
配置详情替换功能的默认提示词和参数。详情替换可将产品图批量融合到多个场景中。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pt-6">
|
||||
{/* 功能开关 */}
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-base">启用场景裂变功能</Label>
|
||||
<Label className="text-base">启用详情替换功能</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
开启后用户可在图片编辑器中使用场景裂变功能
|
||||
开启后用户可在图片编辑器中使用详情替换功能
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -3932,7 +3932,7 @@ export function AdminSettingsClient() {
|
||||
disabled={!sceneFusionConfig.enabled}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
该提示词将作为场景裂变的基础指令,用户可以在此基础上补充关键词
|
||||
该提示词将作为详情替换的基础指令,用户可以在此基础上添加修改要求
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3997,7 +3997,7 @@ export function AdminSettingsClient() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
选择用于场景裂变的生图模型,支持阿里生图、Gemini、既梦等图片模型
|
||||
选择用于详情替换的生图模型,支持阿里生图、Gemini、既梦等图片模型
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -109,7 +109,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
'extend': '图片扩展',
|
||||
'resize': '尺寸调整',
|
||||
'angle': '角度编辑',
|
||||
'scene-fusion': '场景裂变',
|
||||
'scene-fusion': '详情替换',
|
||||
};
|
||||
return names[action] || action;
|
||||
};
|
||||
@ -428,7 +428,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleAction('scene-fusion')}>
|
||||
<Layers className="h-4 w-4 mr-2" />
|
||||
场景裂变
|
||||
详情替换
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleDownload}>
|
||||
@ -465,7 +465,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 场景裂变对话框 */}
|
||||
{/* 详情替换对话框 */}
|
||||
<SceneFusionDialog
|
||||
open={sceneFusionDialogOpen}
|
||||
onOpenChange={setSceneFusionDialogOpen}
|
||||
|
||||
@ -1297,18 +1297,18 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowSceneFusion(!showSceneFusion);
|
||||
// 进入场景裂变时,退出其他模式
|
||||
// 进入详情替换时,退出其他模式
|
||||
if (!showSceneFusion) {
|
||||
setIsResizeMode(false);
|
||||
setShowAngleEditDialog(false);
|
||||
}
|
||||
}}
|
||||
disabled={showAngleEditDialog || isRemovingBg || isEnhancing || isExtending || isResizeMode}
|
||||
title="场景裂变"
|
||||
title="详情替换"
|
||||
className={showSceneFusion ? "bg-primary text-white" : "text-zinc-300 hover:text-white hover:bg-zinc-800"}
|
||||
>
|
||||
<Layers className="mr-1 h-4 w-4" />
|
||||
场景裂变
|
||||
详情替换
|
||||
</Button>
|
||||
{!showSceneFusion && (
|
||||
<>
|
||||
|
||||
@ -36,7 +36,8 @@ const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
export interface SceneItem {
|
||||
id: string;
|
||||
image: string; // 场景图 URL 或 Base64
|
||||
customPrompt: string; // 该场景的补充关键词
|
||||
customPrompt: string; // 该场景的修改要求
|
||||
retryCount?: number; // 重试次数
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
result?: string; // 生成结果缩略图 URL
|
||||
originalUrl?: string; // 生成结果原图 URL(用于下载)
|
||||
@ -49,7 +50,10 @@ interface SceneFusionDialogProps {
|
||||
}
|
||||
|
||||
export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps) {
|
||||
// 获取场景裂变配置(使用公开 API 端点)
|
||||
// 最大自动重试次数
|
||||
const MAX_AUTO_RETRY = 10;
|
||||
|
||||
// 获取详情替换配置(使用公开 API 端点)
|
||||
const { data: configData } = useSWR(
|
||||
open ? "/api/scene-fusion/config" : null,
|
||||
fetcher
|
||||
@ -140,7 +144,7 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps
|
||||
// 开始裂变
|
||||
const startFusion = async () => {
|
||||
if (config?.enabled === false) {
|
||||
toast.error("场景裂变功能已被管理员禁用");
|
||||
toast.error("详情替换功能已被管理员禁用");
|
||||
return;
|
||||
}
|
||||
if (!productImage) {
|
||||
@ -226,16 +230,19 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps
|
||||
}
|
||||
};
|
||||
|
||||
// 重新生成单个场景
|
||||
const regenerateScene = async (id: string) => {
|
||||
// 重新生成单个场景(带自动重试)
|
||||
const regenerateScene = async (id: string, retryCount: number = 0) => {
|
||||
if (!productImage) return;
|
||||
|
||||
const scene = scenes.find(s => s.id === id);
|
||||
if (!scene) return;
|
||||
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, status: 'processing', error: undefined } : s
|
||||
));
|
||||
// 只在首次调用时显示处理状态
|
||||
if (retryCount === 0) {
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, status: 'processing', error: undefined, retryCount: 0 } : s
|
||||
));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/scene-fusion', {
|
||||
@ -300,9 +307,10 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps
|
||||
status: 'completed',
|
||||
result: result.result,
|
||||
originalUrl: result.originalUrl,
|
||||
retryCount: 0,
|
||||
} : s
|
||||
));
|
||||
toast.success('重新生成成功');
|
||||
toast.success('生成成功');
|
||||
success = true;
|
||||
} else {
|
||||
throw new Error(result.error || '生成失败');
|
||||
@ -325,11 +333,25 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps
|
||||
throw new Error('未收到有效的生成结果');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SceneFusion] Regenerate error:', error);
|
||||
console.error(`[SceneFusion] Regenerate error (retry ${retryCount}/${MAX_AUTO_RETRY}):`, error);
|
||||
|
||||
// 检查是否需要自动重试
|
||||
if (retryCount < MAX_AUTO_RETRY) {
|
||||
console.log(`[SceneFusion] Auto-retrying scene ${id} (${retryCount + 1}/${MAX_AUTO_RETRY})...`);
|
||||
// 更新重试计数(用户无感)
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, retryCount: retryCount + 1 } : s
|
||||
));
|
||||
// 延迟2秒后自动重试
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
return regenerateScene(id, retryCount + 1);
|
||||
}
|
||||
|
||||
// 达到最大重试次数,显示失败
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, status: 'failed', error: error instanceof Error ? error.message : '处理失败' } : s
|
||||
s.id === id ? { ...s, status: 'failed', error: `重试${MAX_AUTO_RETRY}次后失败: ${error instanceof Error ? error.message : '处理失败'}` } : s
|
||||
));
|
||||
toast.error(error instanceof Error ? error.message : '重新生成失败');
|
||||
toast.error(`重试${MAX_AUTO_RETRY}次后仍然失败`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -388,7 +410,7 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps
|
||||
<DialogHeader className="px-6 py-4 border-b shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Layers className="w-5 h-5 text-primary" />
|
||||
场景裂变
|
||||
详情替换
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
上传1张产品图和多张场景图,AI 自动将产品融合到所有场景中。
|
||||
@ -505,7 +527,7 @@ export function SceneFusionDialog({ open, onOpenChange }: SceneFusionDialogProps
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
placeholder="补充关键词:如'放在木桌上'"
|
||||
placeholder="修改要求:如'放在木桌上'"
|
||||
value={scene.customPrompt}
|
||||
onChange={(e) => updateScenePrompt(scene.id, e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
|
||||
@ -27,7 +27,8 @@ const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
export interface SceneItem {
|
||||
id: string;
|
||||
image: string; // 场景图 URL 或 Base64
|
||||
customPrompt: string; // 该场景的补充关键词
|
||||
customPrompt: string; // 该场景的修改要求
|
||||
retryCount?: number; // 重试次数
|
||||
targetName: string; // 背景中需要被替换的物体名称
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
result?: string; // 生成结果缩略图 URL
|
||||
@ -40,7 +41,10 @@ interface SceneFusionViewProps {
|
||||
}
|
||||
|
||||
export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
// 获取场景裂变配置(使用公开 API 端点)
|
||||
// 最大自动重试次数
|
||||
const MAX_AUTO_RETRY = 10;
|
||||
|
||||
// 获取详情替换配置(使用公开 API 端点)
|
||||
const { data: configData } = useSWR(
|
||||
"/api/scene-fusion/config",
|
||||
fetcher
|
||||
@ -159,7 +163,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
// 开始裂变
|
||||
const startFusion = async () => {
|
||||
if (config?.enabled === false) {
|
||||
toast.error("场景裂变功能已被管理员禁用");
|
||||
toast.error("详情替换功能已被管理员禁用");
|
||||
return;
|
||||
}
|
||||
if (!productImage) {
|
||||
@ -270,14 +274,29 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
status: 'completed',
|
||||
result: result.result,
|
||||
originalUrl: result.originalUrl,
|
||||
retryCount: 0, // 重置重试计数
|
||||
};
|
||||
} else {
|
||||
failedCount++;
|
||||
return {
|
||||
...s,
|
||||
status: 'failed',
|
||||
error: result.error || '生成失败'
|
||||
};
|
||||
// 检查是否需要自动重试
|
||||
const currentRetryCount = s.retryCount || 0;
|
||||
if (currentRetryCount < MAX_AUTO_RETRY) {
|
||||
console.log(`[SceneFusion] Scene ${result.id} failed, will auto-retry (${currentRetryCount + 1}/${MAX_AUTO_RETRY})`);
|
||||
// 标记为需要重试,稍后会自动重试
|
||||
return {
|
||||
...s,
|
||||
status: 'pending', // 改为 pending 以便重新处理
|
||||
error: undefined,
|
||||
retryCount: currentRetryCount + 1,
|
||||
};
|
||||
} else {
|
||||
failedCount++;
|
||||
console.log(`[SceneFusion] Scene ${result.id} failed after ${MAX_AUTO_RETRY} retries`);
|
||||
return {
|
||||
...s,
|
||||
status: 'failed',
|
||||
error: `重试${MAX_AUTO_RETRY}次后仍然失败: ${result.error || '生成失败'}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
@ -286,9 +305,9 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
const { summary } = message;
|
||||
console.log('[SceneFusion Client] All done:', summary);
|
||||
|
||||
if (summary.failed > 0) {
|
||||
toast.warning(`批量处理完成:${summary.success} 成功,${summary.failed} 失败`);
|
||||
} else {
|
||||
// 检查是否有需要自动重试的场景
|
||||
// 注意:这里不直接调用,而是在 finally 中检查
|
||||
if (summary.failed === 0) {
|
||||
toast.success(`批量处理完成:${summary.success} 张图片已生成`);
|
||||
}
|
||||
} else if (message.type === 'error') {
|
||||
@ -308,25 +327,57 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
console.error('[SceneFusion] Error:', error);
|
||||
toast.error(error instanceof Error ? error.message : '处理失败');
|
||||
|
||||
// 将 processing 状态改回 failed
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.status === 'processing' ? { ...s, status: 'failed', error: '处理失败' } : s
|
||||
));
|
||||
// 将 processing 状态改回 pending(以便自动重试)或 failed
|
||||
setScenes(prev => prev.map(s => {
|
||||
if (s.status === 'processing') {
|
||||
const currentRetryCount = s.retryCount || 0;
|
||||
if (currentRetryCount < MAX_AUTO_RETRY) {
|
||||
return { ...s, status: 'pending', retryCount: currentRetryCount + 1 };
|
||||
}
|
||||
return { ...s, status: 'failed', error: '处理失败' };
|
||||
}
|
||||
return s;
|
||||
}));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
|
||||
// 检查是否有需要自动重试的场景(状态为 pending 且 retryCount > 0)
|
||||
// 使用 setTimeout 延迟执行,避免立即重试
|
||||
setTimeout(() => {
|
||||
setScenes(currentScenes => {
|
||||
const pendingRetries = currentScenes.filter(s => s.status === 'pending' && (s.retryCount || 0) > 0);
|
||||
if (pendingRetries.length > 0) {
|
||||
console.log(`[SceneFusion] Auto-retrying ${pendingRetries.length} failed scenes...`);
|
||||
// 触发重新处理 - 由于 startFusion 会处理 pending 状态的场景
|
||||
// 我们需要在下一个事件循环中调用
|
||||
setTimeout(() => startFusion(), 100);
|
||||
} else {
|
||||
// 所有场景都处理完成,检查最终结果
|
||||
const failed = currentScenes.filter(s => s.status === 'failed').length;
|
||||
const completed = currentScenes.filter(s => s.status === 'completed').length;
|
||||
if (failed > 0 && completed > 0) {
|
||||
toast.warning(`批量处理完成:${completed} 成功,${failed} 失败`);
|
||||
}
|
||||
}
|
||||
return currentScenes;
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 重新生成单个场景
|
||||
const regenerateScene = async (id: string) => {
|
||||
// 重新生成单个场景(带自动重试)
|
||||
const regenerateScene = async (id: string, retryCount: number = 0) => {
|
||||
if (!productImage) return;
|
||||
|
||||
const scene = scenes.find(s => s.id === id);
|
||||
if (!scene) return;
|
||||
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, status: 'processing', error: undefined } : s
|
||||
));
|
||||
// 只在首次调用时显示处理状态
|
||||
if (retryCount === 0) {
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, status: 'processing', error: undefined, retryCount: 0 } : s
|
||||
));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/scene-fusion', {
|
||||
@ -395,9 +446,10 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
status: 'completed',
|
||||
result: result.result,
|
||||
originalUrl: result.originalUrl,
|
||||
retryCount: 0,
|
||||
} : s
|
||||
));
|
||||
toast.success('重新生成成功');
|
||||
toast.success('生成成功');
|
||||
success = true;
|
||||
} else {
|
||||
throw new Error(result.error || '生成失败');
|
||||
@ -420,9 +472,23 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
throw new Error('未收到有效的生成结果');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SceneFusion] Regenerate error:', error);
|
||||
console.error(`[SceneFusion] Regenerate error (retry ${retryCount}/${MAX_AUTO_RETRY}):`, error);
|
||||
|
||||
// 检查是否需要自动重试
|
||||
if (retryCount < MAX_AUTO_RETRY) {
|
||||
console.log(`[SceneFusion] Auto-retrying scene ${id} (${retryCount + 1}/${MAX_AUTO_RETRY})...`);
|
||||
// 更新重试计数(用户无感)
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, retryCount: retryCount + 1 } : s
|
||||
));
|
||||
// 延迟2秒后自动重试
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
return regenerateScene(id, retryCount + 1);
|
||||
}
|
||||
|
||||
// 达到最大重试次数,显示失败
|
||||
setScenes(prev => prev.map(s =>
|
||||
s.id === id ? { ...s, status: 'failed', error: error instanceof Error ? error.message : '处理失败' } : s
|
||||
s.id === id ? { ...s, status: 'failed', error: `重试${MAX_AUTO_RETRY}次后失败: ${error instanceof Error ? error.message : '处理失败'}` } : s
|
||||
));
|
||||
toast.error(error instanceof Error ? error.message : '重新生成失败');
|
||||
}
|
||||
@ -488,7 +554,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Layers className="w-5 h-5 text-primary" />
|
||||
场景裂变
|
||||
详情替换
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground hidden md:block">
|
||||
@ -678,7 +744,7 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<Input
|
||||
placeholder="补充关键词(可选)"
|
||||
placeholder="修改要求(可选)"
|
||||
value={scene.customPrompt}
|
||||
onChange={(e) => updateScenePrompt(scene.id, e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user