diff --git a/app/api/realtime/config/route.ts b/app/api/realtime/config/route.ts index 5b5235c39..3ead00588 100644 --- a/app/api/realtime/config/route.ts +++ b/app/api/realtime/config/route.ts @@ -34,7 +34,7 @@ const defaultConfig = { jpegQuality: 0.55, }, model: { - id: "runware:400@4", + id: "runware:101@1", defaults: { steps: 4, cfgScale: 3.5, diff --git a/app/api/realtime/generate/route.ts b/app/api/realtime/generate/route.ts index a2126d733..09cfbc5ca 100644 --- a/app/api/realtime/generate/route.ts +++ b/app/api/realtime/generate/route.ts @@ -108,7 +108,10 @@ export async function POST(request: Request) { ); } - const model = String(config.runwareModel || process.env.RUNWARE_MODEL || "runware:400@4").trim(); + const fallbackSeedImageModel = "runware:101@1"; + const model = String( + config.runwareModel || process.env.RUNWARE_MODEL || fallbackSeedImageModel + ).trim(); const maxInputResolution = clamp(Number(config.maxInputResolution || 1024), 512, 1536); const size = resolveSquareSize(body.imageSize, maxInputResolution); const steps = clamp( @@ -127,56 +130,85 @@ export async function POST(request: Request) { 1 ); - const taskUUID = crypto.randomUUID(); - const runwareRequest = [ - { - taskType: "imageInference", + const positivePrompt = + String(body.prompt || "").trim() || "将草图渲染为高质感产品设计图,保持结构和主轮廓一致。"; + + const requestInference = async (modelId: string) => { + const taskUUID = crypto.randomUUID(); + const runwareRequest = [ + { + taskType: "imageInference", + taskUUID, + model: modelId, + positivePrompt, + seedImage: imageUrl, + width: size, + height: size, + strength, + CFGScale: cfgScale, + steps, + numberResults: 1, + outputType: "dataURI", + outputFormat: "JPG", + }, + ]; + + const upstreamResponse = await fetch("https://api.runware.ai/v1", { + method: "POST", + headers: { + Authorization: `Bearer ${runwareApiKey}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(runwareRequest), + cache: "no-store", + }); + + const upstreamJson = await upstreamResponse.json().catch(() => null); + const upstreamError = parseRunwareError(upstreamJson); + + return { taskUUID, - model, - positivePrompt: String(body.prompt || "").trim() || "将草图渲染为高质感产品设计图,保持结构和主轮廓一致。", - seedImage: imageUrl, - width: size, - height: size, - strength, - CFGScale: cfgScale, - steps, - numberResults: 1, - outputType: "dataURI", - outputFormat: "JPG", - }, - ]; + modelId, + upstreamResponse, + upstreamJson, + upstreamError, + }; + }; - const upstreamResponse = await fetch("https://api.runware.ai/v1", { - method: "POST", - headers: { - Authorization: `Bearer ${runwareApiKey}`, - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(runwareRequest), - cache: "no-store", - }); + let requestResult = await requestInference(model); + const shouldRetryWithSeedImageModel = + (requestResult.upstreamError || "") + .toLowerCase() + .includes("unsupported use of 'seedimage' parameter") && + model !== fallbackSeedImageModel; - const upstreamJson = await upstreamResponse.json().catch(() => null); - const upstreamError = parseRunwareError(upstreamJson); + if (shouldRetryWithSeedImageModel) { + requestResult = await requestInference(fallbackSeedImageModel); + } - if (!upstreamResponse.ok || upstreamError) { + if (!requestResult.upstreamResponse.ok || requestResult.upstreamError) { return Response.json( { code: "RUNWARE_REQUEST_FAILED", - message: upstreamError || `Runware 请求失败(${upstreamResponse.status})`, + message: + requestResult.upstreamError || + `Runware 请求失败(${requestResult.upstreamResponse.status})`, }, { status: 502 } ); } - const resultList = Array.isArray(upstreamJson) - ? upstreamJson - : Array.isArray(upstreamJson?.data) - ? upstreamJson.data + const resultList = Array.isArray(requestResult.upstreamJson) + ? requestResult.upstreamJson + : Array.isArray(requestResult.upstreamJson?.data) + ? requestResult.upstreamJson.data : []; const imageResult = - resultList.find((item: any) => item?.taskUUID === taskUUID && (item?.imageDataURI || item?.imageURL)) || + resultList.find( + (item: any) => + item?.taskUUID === requestResult.taskUUID && (item?.imageDataURI || item?.imageURL) + ) || resultList.find((item: any) => item?.imageDataURI || item?.imageURL); const imageDataURI = String(imageResult?.imageDataURI || "").trim(); @@ -194,16 +226,16 @@ export async function POST(request: Request) { console.log("[RealtimeRunware] frame success", { userId: session.user.id, requestId: body.requestId || null, - taskUUID, - model, + taskUUID: requestResult.taskUUID, + model: requestResult.modelId, cost, }); return Response.json({ ok: true, provider: "runware", - model, - taskUUID, + model: requestResult.modelId, + taskUUID: requestResult.taskUUID, imageDataURI: imageDataURI || undefined, imageUrl: imageURL || undefined, cost, diff --git a/components/canvas/RealtimeCanvasDialog.tsx b/components/canvas/RealtimeCanvasDialog.tsx index 9d068c92d..300d32af9 100644 --- a/components/canvas/RealtimeCanvasDialog.tsx +++ b/components/canvas/RealtimeCanvasDialog.tsx @@ -89,7 +89,7 @@ const defaultRealtimeConfig: RealtimeConfig = { jpegQuality: 0.55, }, model: { - id: "runware:400@4", + id: "runware:101@1", defaults: { steps: 4, cfgScale: 3.5,