feat: implement streaming response for scene fusion - show images as they complete
This commit is contained in:
parent
b8efd6b096
commit
bec71cf195
@ -678,87 +678,118 @@ export async function POST(request: Request) {
|
|||||||
// 6. 串行处理所有场景,每次间隔5秒
|
// 6. 串行处理所有场景,每次间隔5秒
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (let i = 0; i < processedScenes.length; i++) {
|
// 6. 创建流式响应,每完成一张图就立即发送给前端
|
||||||
const scene = processedScenes[i];
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
console.log(`[SceneFusion] ========== Processing scene ${i + 1}/${processedScenes.length} ==========`);
|
// 发送初始消息
|
||||||
console.log(`[SceneFusion] Scene ID: ${scene.id}`);
|
controller.enqueue(encoder.encode(JSON.stringify({ type: "start", total: processedScenes.length }) + "\n"));
|
||||||
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)'}`);
|
|
||||||
console.log(`[SceneFusion] Provider Type: ${provider.type}`);
|
|
||||||
|
|
||||||
// 如果不是第一张图,等待5秒
|
let successCount = 0;
|
||||||
if (i > 0) {
|
let failCount = 0;
|
||||||
console.log(`[SceneFusion] Waiting 5 seconds before processing...`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
for (let i = 0; i < processedScenes.length; i++) {
|
||||||
// 使用新的通用处理函数,支持多种模型类型
|
const scene = processedScenes[i];
|
||||||
const result = await processSceneWithProvider(
|
|
||||||
productImageUrl,
|
|
||||||
scene.imageUrl,
|
|
||||||
productName,
|
|
||||||
scene.targetName,
|
|
||||||
scene.customPrompt,
|
|
||||||
config.defaultPrompt,
|
|
||||||
{ type: provider.type, baseUrl: provider.baseUrl, apiKey: provider.apiKey },
|
|
||||||
model.modelId,
|
|
||||||
aspectRatio,
|
|
||||||
imageSize
|
|
||||||
);
|
|
||||||
|
|
||||||
// 将分辨率等级用于数据库存储
|
console.log(`[SceneFusion] ========== Processing scene ${i + 1}/${processedScenes.length} ==========`);
|
||||||
const dbImageSize = imageSize || "2K";
|
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)'}`);
|
||||||
|
console.log(`[SceneFusion] Provider Type: ${provider.type}`);
|
||||||
|
|
||||||
// 保存到数据库(chatId 和 messageId 设为 null,因为场景裂变不关联聊天)
|
// 如果不是第一张图,等待5秒
|
||||||
try {
|
if (i > 0) {
|
||||||
await db.insert(generatedImage).values({
|
console.log(`[SceneFusion] Waiting 5 seconds before processing...`);
|
||||||
userId: session.user.id,
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
chatId: null, // 场景裂变不关联聊天
|
}
|
||||||
messageId: null, // 场景裂变不关联消息
|
|
||||||
thumbnailUrl: result.thumbnail,
|
try {
|
||||||
originalUrl: result.original,
|
// 使用新的通用处理函数,支持多种模型类型
|
||||||
imageSize: dbImageSize,
|
const result = await processSceneWithProvider(
|
||||||
prompt: `[场景裂变] ${result.prompt}`,
|
productImageUrl,
|
||||||
modelId: model.modelId,
|
scene.imageUrl,
|
||||||
});
|
productName,
|
||||||
console.log(`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`);
|
scene.targetName,
|
||||||
} catch (dbError) {
|
scene.customPrompt,
|
||||||
console.error("[SceneFusion] ❌ Failed to save to gallery:", dbError);
|
config.defaultPrompt,
|
||||||
|
{ type: provider.type, baseUrl: provider.baseUrl, apiKey: provider.apiKey },
|
||||||
|
model.modelId,
|
||||||
|
aspectRatio,
|
||||||
|
imageSize
|
||||||
|
);
|
||||||
|
|
||||||
|
// 将分辨率等级用于数据库存储
|
||||||
|
const dbImageSize = imageSize || "2K";
|
||||||
|
|
||||||
|
// 保存到数据库(chatId 和 messageId 设为 null,因为场景裂变不关联聊天)
|
||||||
|
try {
|
||||||
|
await db.insert(generatedImage).values({
|
||||||
|
userId: session.user.id,
|
||||||
|
chatId: null, // 场景裂变不关联聊天
|
||||||
|
messageId: null, // 场景裂变不关联消息
|
||||||
|
thumbnailUrl: result.thumbnail,
|
||||||
|
originalUrl: result.original,
|
||||||
|
imageSize: dbImageSize,
|
||||||
|
prompt: `[场景裂变] ${result.prompt}`,
|
||||||
|
modelId: model.modelId,
|
||||||
|
});
|
||||||
|
console.log(`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`);
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error("[SceneFusion] ❌ Failed to save to gallery:", dbError);
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
// 立即发送完成的结果给前端
|
||||||
|
controller.enqueue(encoder.encode(JSON.stringify({
|
||||||
|
type: "result",
|
||||||
|
data: {
|
||||||
|
id: scene.id,
|
||||||
|
status: "completed" as const,
|
||||||
|
result: result.thumbnail, // 返回缩略图用于预览
|
||||||
|
originalUrl: result.original, // 原图用于下载
|
||||||
|
},
|
||||||
|
}) + "\n"));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[SceneFusion] Failed to process scene ${scene.id}:`, error);
|
||||||
|
failCount++;
|
||||||
|
|
||||||
|
// 立即发送失败的结果给前端
|
||||||
|
controller.enqueue(encoder.encode(JSON.stringify({
|
||||||
|
type: "result",
|
||||||
|
data: {
|
||||||
|
id: scene.id,
|
||||||
|
status: "failed" as const,
|
||||||
|
error: error instanceof Error ? error.message : "处理失败",
|
||||||
|
},
|
||||||
|
}) + "\n"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({
|
// 发送完成消息
|
||||||
id: scene.id,
|
console.log(`[SceneFusion] Completed: ${successCount} success, ${failCount} failed`);
|
||||||
status: "completed" as const,
|
controller.enqueue(encoder.encode(JSON.stringify({
|
||||||
result: result.thumbnail, // 返回缩略图用于预览
|
type: "done",
|
||||||
originalUrl: result.original, // 原图用于下载
|
summary: {
|
||||||
});
|
total: processedScenes.length,
|
||||||
} catch (error) {
|
success: successCount,
|
||||||
console.error(`[SceneFusion] Failed to process scene ${scene.id}:`, error);
|
failed: failCount,
|
||||||
results.push({
|
},
|
||||||
id: scene.id,
|
}) + "\n"));
|
||||||
status: "failed" as const,
|
|
||||||
error: error instanceof Error ? error.message : "处理失败",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 返回结果
|
controller.close();
|
||||||
const successCount = results.filter((r) => r.status === "completed").length;
|
},
|
||||||
const failCount = results.filter((r) => r.status === "failed").length;
|
});
|
||||||
|
|
||||||
console.log(`[SceneFusion] Completed: ${successCount} success, ${failCount} failed`);
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
return NextResponse.json({
|
"Content-Type": "text/event-stream",
|
||||||
success: true,
|
"Cache-Control": "no-cache",
|
||||||
results,
|
"Connection": "keep-alive",
|
||||||
summary: {
|
|
||||||
total: scenes.length,
|
|
||||||
success: successCount,
|
|
||||||
failed: failCount,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -182,13 +182,13 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[SceneFusion Client] Starting API call with', scenesToProcess.length, 'scenes');
|
console.log('[SceneFusion Client] Starting streaming API call with', scenesToProcess.length, 'scenes');
|
||||||
|
|
||||||
// 创建 AbortController 用于超时控制(10分钟)
|
// 创建 AbortController 用于超时控制(10分钟)
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000);
|
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000);
|
||||||
|
|
||||||
// 调用后端 API
|
// 调用后端 API(流式响应)
|
||||||
const response = await fetch('/api/scene-fusion', {
|
const response = await fetch('/api/scene-fusion', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -212,42 +212,90 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.log('[SceneFusion Client] Response received, status:', response.status);
|
console.log('[SceneFusion Client] Response received, status:', response.status);
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('[SceneFusion Client] Response data:', JSON.stringify(data).substring(0, 500));
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.error || '处理失败');
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '处理失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新场景状态
|
// 流式读取响应
|
||||||
console.log('[SceneFusion Client] Updating scenes with results:', data.results?.length);
|
const reader = response.body?.getReader();
|
||||||
setScenes(prev => prev.map(s => {
|
const decoder = new TextDecoder();
|
||||||
const result = data.results?.find((r: any) => r.id === s.id);
|
|
||||||
if (result) {
|
if (!reader) {
|
||||||
console.log('[SceneFusion Client] Scene', s.id, 'result:', result.status);
|
throw new Error('无法读取响应流');
|
||||||
if (result.status === 'completed') {
|
}
|
||||||
return {
|
|
||||||
...s,
|
let buffer = '';
|
||||||
status: 'completed',
|
let totalScenes = 0;
|
||||||
result: result.result,
|
let completedCount = 0;
|
||||||
originalUrl: result.originalUrl,
|
let failedCount = 0;
|
||||||
};
|
|
||||||
} else {
|
while (true) {
|
||||||
return {
|
const { done, value } = await reader.read();
|
||||||
...s,
|
|
||||||
status: 'failed',
|
if (done) {
|
||||||
error: result.error || '生成失败'
|
console.log('[SceneFusion Client] Stream completed');
|
||||||
};
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码数据并添加到缓冲区
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// 处理完整的行(以\n分隔)
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // 保留最后一个不完整的行
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
console.log('[SceneFusion Client] Received message:', message.type);
|
||||||
|
|
||||||
|
if (message.type === 'start') {
|
||||||
|
totalScenes = message.total;
|
||||||
|
console.log('[SceneFusion Client] Total scenes:', totalScenes);
|
||||||
|
} else if (message.type === 'result') {
|
||||||
|
const result = message.data;
|
||||||
|
console.log('[SceneFusion Client] Scene result:', result.id, result.status);
|
||||||
|
|
||||||
|
// 立即更新对应场景的状态
|
||||||
|
setScenes(prev => prev.map(s => {
|
||||||
|
if (s.id === result.id) {
|
||||||
|
if (result.status === 'completed') {
|
||||||
|
completedCount++;
|
||||||
|
toast.success(`场景 ${completedCount} 生成完成!`);
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
status: 'completed',
|
||||||
|
result: result.result,
|
||||||
|
originalUrl: result.originalUrl,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
failedCount++;
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
status: 'failed',
|
||||||
|
error: result.error || '生成失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}));
|
||||||
|
} else if (message.type === 'done') {
|
||||||
|
const { summary } = message;
|
||||||
|
console.log('[SceneFusion Client] All done:', summary);
|
||||||
|
|
||||||
|
if (summary.failed > 0) {
|
||||||
|
toast.warning(`批量处理完成:${summary.success} 成功,${summary.failed} 失败`);
|
||||||
|
} else {
|
||||||
|
toast.success(`批量处理完成:${summary.success} 张图片已生成`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('[SceneFusion Client] Failed to parse message:', parseError, 'Line:', line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s;
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { summary } = data;
|
|
||||||
if (summary.failed > 0) {
|
|
||||||
toast.warning(`批量处理完成:${summary.success} 成功,${summary.failed} 失败`);
|
|
||||||
} else {
|
|
||||||
toast.success(`批量处理完成:${summary.success} 张图片已生成`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SceneFusion] Error:', error);
|
console.error('[SceneFusion] Error:', error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user