fix(realtime-canvas): use runware seedImage-compatible model
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-25 15:48:16 +08:00
parent 6c30ab90ef
commit 80e826039f
3 changed files with 75 additions and 43 deletions

View File

@ -34,7 +34,7 @@ const defaultConfig = {
jpegQuality: 0.55, jpegQuality: 0.55,
}, },
model: { model: {
id: "runware:400@4", id: "runware:101@1",
defaults: { defaults: {
steps: 4, steps: 4,
cfgScale: 3.5, cfgScale: 3.5,

View File

@ -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 maxInputResolution = clamp(Number(config.maxInputResolution || 1024), 512, 1536);
const size = resolveSquareSize(body.imageSize, maxInputResolution); const size = resolveSquareSize(body.imageSize, maxInputResolution);
const steps = clamp( const steps = clamp(
@ -127,56 +130,85 @@ export async function POST(request: Request) {
1 1
); );
const taskUUID = crypto.randomUUID(); const positivePrompt =
const runwareRequest = [ String(body.prompt || "").trim() || "将草图渲染为高质感产品设计图,保持结构和主轮廓一致。";
{
taskType: "imageInference", 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, taskUUID,
model, modelId,
positivePrompt: String(body.prompt || "").trim() || "将草图渲染为高质感产品设计图,保持结构和主轮廓一致。", upstreamResponse,
seedImage: imageUrl, upstreamJson,
width: size, upstreamError,
height: size, };
strength, };
CFGScale: cfgScale,
steps,
numberResults: 1,
outputType: "dataURI",
outputFormat: "JPG",
},
];
const upstreamResponse = await fetch("https://api.runware.ai/v1", { let requestResult = await requestInference(model);
method: "POST", const shouldRetryWithSeedImageModel =
headers: { (requestResult.upstreamError || "")
Authorization: `Bearer ${runwareApiKey}`, .toLowerCase()
"Content-Type": "application/json", .includes("unsupported use of 'seedimage' parameter") &&
Accept: "application/json", model !== fallbackSeedImageModel;
},
body: JSON.stringify(runwareRequest),
cache: "no-store",
});
const upstreamJson = await upstreamResponse.json().catch(() => null); if (shouldRetryWithSeedImageModel) {
const upstreamError = parseRunwareError(upstreamJson); requestResult = await requestInference(fallbackSeedImageModel);
}
if (!upstreamResponse.ok || upstreamError) { if (!requestResult.upstreamResponse.ok || requestResult.upstreamError) {
return Response.json( return Response.json(
{ {
code: "RUNWARE_REQUEST_FAILED", code: "RUNWARE_REQUEST_FAILED",
message: upstreamError || `Runware 请求失败(${upstreamResponse.status})`, message:
requestResult.upstreamError ||
`Runware 请求失败(${requestResult.upstreamResponse.status})`,
}, },
{ status: 502 } { status: 502 }
); );
} }
const resultList = Array.isArray(upstreamJson) const resultList = Array.isArray(requestResult.upstreamJson)
? upstreamJson ? requestResult.upstreamJson
: Array.isArray(upstreamJson?.data) : Array.isArray(requestResult.upstreamJson?.data)
? upstreamJson.data ? requestResult.upstreamJson.data
: []; : [];
const imageResult = 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); resultList.find((item: any) => item?.imageDataURI || item?.imageURL);
const imageDataURI = String(imageResult?.imageDataURI || "").trim(); const imageDataURI = String(imageResult?.imageDataURI || "").trim();
@ -194,16 +226,16 @@ export async function POST(request: Request) {
console.log("[RealtimeRunware] frame success", { console.log("[RealtimeRunware] frame success", {
userId: session.user.id, userId: session.user.id,
requestId: body.requestId || null, requestId: body.requestId || null,
taskUUID, taskUUID: requestResult.taskUUID,
model, model: requestResult.modelId,
cost, cost,
}); });
return Response.json({ return Response.json({
ok: true, ok: true,
provider: "runware", provider: "runware",
model, model: requestResult.modelId,
taskUUID, taskUUID: requestResult.taskUUID,
imageDataURI: imageDataURI || undefined, imageDataURI: imageDataURI || undefined,
imageUrl: imageURL || undefined, imageUrl: imageURL || undefined,
cost, cost,

View File

@ -89,7 +89,7 @@ const defaultRealtimeConfig: RealtimeConfig = {
jpegQuality: 0.55, jpegQuality: 0.55,
}, },
model: { model: {
id: "runware:400@4", id: "runware:101@1",
defaults: { defaults: {
steps: 4, steps: 4,
cfgScale: 3.5, cfgScale: 3.5,