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秒
|
||||
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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user