From bec71cf1953e496b13f4dae9aaf7b20ddf6ef7aa Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 22 Jan 2026 19:33:06 +0800 Subject: [PATCH] feat: implement streaming response for scene fusion - show images as they complete --- app/api/scene-fusion/route.ts | 183 ++++++++++++++++++------------- components/scene-fusion-view.tsx | 114 +++++++++++++------ 2 files changed, 188 insertions(+), 109 deletions(-) diff --git a/app/api/scene-fusion/route.ts b/app/api/scene-fusion/route.ts index 672e9dc..4314177 100644 --- a/app/api/scene-fusion/route.ts +++ b/app/api/scene-fusion/route.ts @@ -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) { diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index f55c5b6..560f4a8 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -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);