feat: implement streaming response for scene fusion - show images as they complete

This commit is contained in:
Your Name 2026-01-22 19:33:06 +08:00
parent b8efd6b096
commit bec71cf195
2 changed files with 188 additions and 109 deletions

View File

@ -678,87 +678,118 @@ export async function POST(request: Request) {
// 6. 串行处理所有场景,每次间隔5秒
const results = [];
for (let i = 0; i < processedScenes.length; i++) {
const scene = processedScenes[i];
console.log(`[SceneFusion] ========== Processing scene ${i + 1}/${processedScenes.length} ==========`);
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}`);
// 如果不是第一张图,等待5秒
if (i > 0) {
console.log(`[SceneFusion] Waiting 5 seconds before processing...`);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
try {
// 使用新的通用处理函数,支持多种模型类型
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
);
// 6. 创建流式响应,每完成一张图就立即发送给前端
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
// 将分辨率等级用于数据库存储
const dbImageSize = imageSize || "2K";
// 发送初始消息
controller.enqueue(encoder.encode(JSON.stringify({ type: "start", total: processedScenes.length }) + "\n"));
// 保存到数据库(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);
let successCount = 0;
let failCount = 0;
for (let i = 0; i < processedScenes.length; i++) {
const scene = processedScenes[i];
console.log(`[SceneFusion] ========== Processing scene ${i + 1}/${processedScenes.length} ==========`);
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}`);
// 如果不是第一张图,等待5秒
if (i > 0) {
console.log(`[SceneFusion] Waiting 5 seconds before processing...`);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
try {
// 使用新的通用处理函数,支持多种模型类型
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
);
// 将分辨率等级用于数据库存储
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,
status: "completed" as const,
result: result.thumbnail, // 返回缩略图用于预览
originalUrl: result.original, // 原图用于下载
});
} catch (error) {
console.error(`[SceneFusion] Failed to process scene ${scene.id}:`, error);
results.push({
id: scene.id,
status: "failed" as const,
error: error instanceof Error ? error.message : "处理失败",
});
}
}
// 发送完成消息
console.log(`[SceneFusion] Completed: ${successCount} success, ${failCount} failed`);
controller.enqueue(encoder.encode(JSON.stringify({
type: "done",
summary: {
total: processedScenes.length,
success: successCount,
failed: failCount,
},
}) + "\n"));
controller.close();
},
});
// 6. 返回结果
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 NextResponse.json({
success: true,
results,
summary: {
total: scenes.length,
success: successCount,
failed: failCount,
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
} catch (error) {

View File

@ -182,13 +182,13 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
));
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分钟)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000);
// 调用后端 API
// 调用后端 API(流式响应)
const response = await fetch('/api/scene-fusion', {
method: 'POST',
headers: {
@ -212,42 +212,90 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
clearTimeout(timeoutId);
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) {
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);
setScenes(prev => prev.map(s => {
const result = data.results?.find((r: any) => r.id === s.id);
if (result) {
console.log('[SceneFusion Client] Scene', s.id, 'result:', result.status);
if (result.status === 'completed') {
return {
...s,
status: 'completed',
result: result.result,
originalUrl: result.originalUrl,
};
} else {
return {
...s,
status: 'failed',
error: result.error || '生成失败'
};
// 流式读取响应
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('无法读取响应流');
}
let buffer = '';
let totalScenes = 0;
let completedCount = 0;
let failedCount = 0;
while (true) {
const { done, value } = await reader.read();
if (done) {
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) {
console.error('[SceneFusion] Error:', error);