fix(image-tools): restore dedicated processing fallbacks
This commit is contained in:
parent
835b60f61c
commit
4a6afb753d
@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
createMediaAssetRelation,
|
||||
getAiProvidersByType,
|
||||
getMediaAssetsByUserAndUrls,
|
||||
getSystemSetting,
|
||||
upsertMediaAsset,
|
||||
@ -10,6 +11,7 @@ import { uploadFile } from "@/lib/storage/service";
|
||||
import { db } from "@/lib/db";
|
||||
import { generatedImage } from "@/lib/db/schema";
|
||||
import { executeConfiguredVip98kTask } from "@/lib/ai/vip98k-runtime";
|
||||
import { createFalClient } from "@fal-ai/client";
|
||||
|
||||
// 变清晰API配置类型
|
||||
type EnhanceApiConfig = {
|
||||
@ -319,6 +321,59 @@ async function processWithRealESRGANBase64(
|
||||
};
|
||||
}
|
||||
|
||||
async function processWithFalEsrgan(
|
||||
imageBase64: string,
|
||||
requestedScaleFactor: number,
|
||||
): Promise<{ imageBase64: string; processingTime: number }> {
|
||||
const providers = await getAiProvidersByType({
|
||||
type: "fal-image",
|
||||
enabledOnly: true,
|
||||
});
|
||||
const provider = providers.find((item) => item.apiKey?.trim());
|
||||
if (!provider?.apiKey) {
|
||||
throw new Error("未配置可用的 FAL 专用高清服务");
|
||||
}
|
||||
|
||||
const scale = Math.max(2, Math.min(8, requestedScaleFactor || 4));
|
||||
const model = scale <= 2 ? "RealESRGAN_x2plus" : "RealESRGAN_x4plus";
|
||||
const credentials = provider.apiKey
|
||||
.trim()
|
||||
.replace(/^(?:Key|Bearer)\s+/i, "");
|
||||
const fal = createFalClient({ credentials });
|
||||
const startedAt = Date.now();
|
||||
const result: any = await fal.subscribe("fal-ai/esrgan", {
|
||||
input: {
|
||||
image_url: `data:image/jpeg;base64,${imageBase64}`,
|
||||
scale,
|
||||
model,
|
||||
output_format: "png",
|
||||
},
|
||||
logs: true,
|
||||
timeout: 180_000,
|
||||
});
|
||||
const outputUrl = result?.data?.image?.url || result?.image?.url;
|
||||
if (!outputUrl) {
|
||||
throw new Error("FAL ESRGAN 专用高清服务未返回图片");
|
||||
}
|
||||
|
||||
const outputResponse = await fetch(outputUrl, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
});
|
||||
if (!outputResponse.ok) {
|
||||
throw new Error(
|
||||
`FAL ESRGAN 高清结果下载失败(HTTP ${outputResponse.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
imageBase64: Buffer.from(await outputResponse.arrayBuffer()).toString(
|
||||
"base64",
|
||||
),
|
||||
processingTime: (Date.now() - startedAt) / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
async function processWithVip98kEnhance(params: {
|
||||
userId: string;
|
||||
imageBase64: string;
|
||||
@ -634,6 +689,42 @@ async function finalizeEnhanceTaskResult(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function executeFalEsrganFallback(params: {
|
||||
userId: string;
|
||||
imageBase64: string;
|
||||
prompt: string;
|
||||
chatId: string | null;
|
||||
messageId: string | null;
|
||||
sourceImageUrl: string | null;
|
||||
requestedScaleFactor?: number;
|
||||
reason: string;
|
||||
}) {
|
||||
const scaleFactor = params.requestedScaleFactor || 4;
|
||||
console.warn("[Enhance-FAL-ESRGAN] 启用专用高清备用链路", {
|
||||
reason: params.reason.slice(0, 500),
|
||||
scaleFactor,
|
||||
});
|
||||
const result = await processWithFalEsrgan(params.imageBase64, scaleFactor);
|
||||
const payload = await finalizeEnhanceTaskResult({
|
||||
userId: params.userId,
|
||||
taskResult: {
|
||||
status: 200,
|
||||
data: { image_base64: result.imageBase64 },
|
||||
},
|
||||
prompt: params.prompt || "图片变清晰处理",
|
||||
chatId: params.chatId,
|
||||
messageId: params.messageId,
|
||||
sourceImageUrls: params.sourceImageUrl
|
||||
? [params.sourceImageUrl]
|
||||
: undefined,
|
||||
});
|
||||
return {
|
||||
...payload,
|
||||
processing_time: result.processingTime,
|
||||
api_type: "fal-esrgan",
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
@ -1026,6 +1117,21 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Enhance-RealESRGAN] ❌ 处理失败:", error);
|
||||
try {
|
||||
const fallbackPayload = await executeFalEsrganFallback({
|
||||
userId,
|
||||
imageBase64,
|
||||
prompt,
|
||||
chatId,
|
||||
messageId,
|
||||
sourceImageUrl,
|
||||
requestedScaleFactor,
|
||||
reason: error instanceof Error ? error.message : "RealESRGAN 备用服务失败",
|
||||
});
|
||||
return NextResponse.json(fallbackPayload);
|
||||
} catch (falError) {
|
||||
console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Real-ESRGAN变清晰处理失败",
|
||||
@ -1180,6 +1286,22 @@ export async function POST(request: NextRequest) {
|
||||
enhanceUrl,
|
||||
processingTime,
|
||||
});
|
||||
|
||||
try {
|
||||
const fallbackPayload = await executeFalEsrganFallback({
|
||||
userId,
|
||||
imageBase64,
|
||||
prompt,
|
||||
chatId,
|
||||
messageId,
|
||||
sourceImageUrl,
|
||||
requestedScaleFactor,
|
||||
reason: fetchError instanceof Error ? fetchError.message : "AOS 高清任务创建失败",
|
||||
});
|
||||
return NextResponse.json(fallbackPayload);
|
||||
} catch (falError) {
|
||||
console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
|
||||
}
|
||||
|
||||
// 检查是否是网络连接错误
|
||||
if (fetchError instanceof Error) {
|
||||
@ -1246,6 +1368,21 @@ export async function POST(request: NextRequest) {
|
||||
return buildEnhanceTaskPendingResponse(taskId);
|
||||
}
|
||||
const detail = pollError instanceof Error ? pollError.message : "轮询失败";
|
||||
try {
|
||||
const fallbackPayload = await executeFalEsrganFallback({
|
||||
userId,
|
||||
imageBase64,
|
||||
prompt,
|
||||
chatId,
|
||||
messageId,
|
||||
sourceImageUrl,
|
||||
requestedScaleFactor,
|
||||
reason: detail,
|
||||
});
|
||||
return NextResponse.json(fallbackPayload);
|
||||
} catch (falError) {
|
||||
console.error("[Enhance-FAL-ESRGAN] 专用高清备用链路失败:", falError);
|
||||
}
|
||||
const friendlyMessage = getAosFriendlyErrorMessage(detail);
|
||||
return NextResponse.json(
|
||||
{ error: "变清晰处理失败", detail: friendlyMessage || detail },
|
||||
|
||||
@ -12,6 +12,10 @@ import {
|
||||
} from "@/lib/db/queries";
|
||||
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||
import { generateCanvasImage } from "@/lib/canvas/generate-image";
|
||||
import { FAL_NANO_BANANA_2_EDIT_MODEL_ID } from "@/lib/ai/fal-image-models";
|
||||
|
||||
export const maxDuration = 600;
|
||||
|
||||
// PicWish API 需要公网可访问的 HTTP/HTTPS URL
|
||||
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
||||
@ -115,11 +119,10 @@ function buildWatermarkTaskPendingResponse(taskId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
async function finalizeWatermarkTaskResult(params: {
|
||||
taskId: string;
|
||||
async function persistWatermarkResult(params: {
|
||||
processedImageUrl: string;
|
||||
userId: string;
|
||||
startedAt?: number;
|
||||
apiKey: string;
|
||||
sourceImageUrls?: string[];
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
@ -127,17 +130,8 @@ async function finalizeWatermarkTaskResult(params: {
|
||||
thumbnail_url: string;
|
||||
processing_time?: number;
|
||||
}> {
|
||||
const pollResult = await pollForResult(params.taskId, params.apiKey, WATERMARK_SYNC_WAIT_TIMEOUT_MS);
|
||||
if (pollResult.pending) {
|
||||
throw new Error("WATERMARK_TASK_PENDING");
|
||||
}
|
||||
if (!pollResult.success || !pollResult.imageUrl) {
|
||||
throw new Error(pollResult.error || "去水印处理失败");
|
||||
}
|
||||
|
||||
const processedImageUrl = pollResult.imageUrl;
|
||||
try {
|
||||
const imageResponse = await fetch(processedImageUrl);
|
||||
const imageResponse = await fetch(params.processedImageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error("下载处理后的图片失败");
|
||||
}
|
||||
@ -217,7 +211,7 @@ async function finalizeWatermarkTaskResult(params: {
|
||||
}
|
||||
}
|
||||
} catch (assetError) {
|
||||
console.error("[WatermarkRemove] ⚠️ MediaAsset 注册失败:", assetError);
|
||||
console.error("[WatermarkRemove] MediaAsset 注册失败:", assetError);
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,14 +226,85 @@ async function finalizeWatermarkTaskResult(params: {
|
||||
console.error("[WatermarkRemove] 上传结果失败,回退原始URL:", uploadError);
|
||||
return {
|
||||
success: true,
|
||||
original_url: processedImageUrl,
|
||||
thumbnail_url: processedImageUrl,
|
||||
original_url: params.processedImageUrl,
|
||||
thumbnail_url: params.processedImageUrl,
|
||||
processing_time:
|
||||
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseFalWatermarkFallback(status: number, detail: string) {
|
||||
return (
|
||||
status === 401 ||
|
||||
status === 402 ||
|
||||
status === 403 ||
|
||||
status === 429 ||
|
||||
status >= 500 ||
|
||||
/quota|credit|balance|exceed|expired|invalid|unauthor/i.test(detail)
|
||||
);
|
||||
}
|
||||
|
||||
async function removeWatermarkWithFal(params: {
|
||||
imageUrl: string;
|
||||
userId: string;
|
||||
startedAt: number;
|
||||
sourceImageUrls?: string[];
|
||||
}) {
|
||||
console.warn("[WatermarkRemove] AOS unavailable; using FAL Nano Banana 2 edit fallback");
|
||||
const result = await generateCanvasImage({
|
||||
prompt: [
|
||||
"仅移除图片中后期叠加的水印、平台角标和覆盖式文字标记。",
|
||||
"必须完整保留原图主体、商品、背景、构图、尺寸、颜色、光影和清晰度。",
|
||||
"用周围真实纹理自然补全被遮挡区域,不得新增文字、标志或其他物体。",
|
||||
].join(""),
|
||||
model: FAL_NANO_BANANA_2_EDIT_MODEL_ID,
|
||||
aspectRatio: "auto",
|
||||
imageSize: "2K",
|
||||
imageCount: 1,
|
||||
referenceImages: [params.imageUrl],
|
||||
allowFailover: false,
|
||||
debugLabel: "watermark-remove-fal-fallback",
|
||||
});
|
||||
const processedImageUrl = result.images[0]?.originalUrl;
|
||||
if (!processedImageUrl) {
|
||||
throw new Error("FAL 去水印未返回可用图片");
|
||||
}
|
||||
return persistWatermarkResult({
|
||||
processedImageUrl,
|
||||
userId: params.userId,
|
||||
startedAt: params.startedAt,
|
||||
sourceImageUrls: params.sourceImageUrls,
|
||||
});
|
||||
}
|
||||
|
||||
async function finalizeWatermarkTaskResult(params: {
|
||||
taskId: string;
|
||||
userId: string;
|
||||
startedAt?: number;
|
||||
apiKey: string;
|
||||
sourceImageUrls?: string[];
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
original_url: string;
|
||||
thumbnail_url: string;
|
||||
processing_time?: number;
|
||||
}> {
|
||||
const pollResult = await pollForResult(params.taskId, params.apiKey, WATERMARK_SYNC_WAIT_TIMEOUT_MS);
|
||||
if (pollResult.pending) {
|
||||
throw new Error("WATERMARK_TASK_PENDING");
|
||||
}
|
||||
if (!pollResult.success || !pollResult.imageUrl) {
|
||||
throw new Error(pollResult.error || "去水印处理失败");
|
||||
}
|
||||
return persistWatermarkResult({
|
||||
processedImageUrl: pollResult.imageUrl,
|
||||
userId: params.userId,
|
||||
startedAt: params.startedAt,
|
||||
sourceImageUrls: params.sourceImageUrls,
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
@ -306,13 +371,6 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// 获取API配置
|
||||
const config = await getWatermarkRemoveConfig();
|
||||
if (!config?.apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "去水印服务未配置,请联系管理员在系统设置中配置API密钥" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log("[WatermarkRemove] 使用原图URL:", image_url.substring(0, 100) + "...");
|
||||
|
||||
// 处理图片URL,确保获取公网可访问的URL
|
||||
@ -396,6 +454,24 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||
}
|
||||
|
||||
const sourceImageUrls =
|
||||
typeof image_url === "string" && /^https?:\/\//i.test(image_url)
|
||||
? [image_url]
|
||||
: undefined;
|
||||
|
||||
if (!config?.apiKey) {
|
||||
const fallbackResult = await removeWatermarkWithFal({
|
||||
imageUrl: publicImageUrl,
|
||||
userId: session.user.id,
|
||||
startedAt: startTime,
|
||||
sourceImageUrls,
|
||||
});
|
||||
if (pointsCharge.chargeable) {
|
||||
await pointsCharge.consume({ note: "FAL 去水印备用链路消耗积分" });
|
||||
}
|
||||
return NextResponse.json(fallbackResult);
|
||||
}
|
||||
|
||||
// 创建去水印任务
|
||||
const formData = new FormData();
|
||||
formData.append("sync", "0");
|
||||
@ -415,8 +491,24 @@ export async function POST(request: NextRequest) {
|
||||
if (!createResponse.ok) {
|
||||
const errorText = await createResponse.text();
|
||||
console.error("[WatermarkRemove] 创建任务失败:", errorText);
|
||||
if (shouldUseFalWatermarkFallback(createResponse.status, errorText)) {
|
||||
try {
|
||||
const fallbackResult = await removeWatermarkWithFal({
|
||||
imageUrl: publicImageUrl,
|
||||
userId: session.user.id,
|
||||
startedAt: startTime,
|
||||
sourceImageUrls,
|
||||
});
|
||||
if (pointsCharge.chargeable) {
|
||||
await pointsCharge.consume({ note: "FAL 去水印备用链路消耗积分" });
|
||||
}
|
||||
return NextResponse.json(fallbackResult);
|
||||
} catch (fallbackError) {
|
||||
console.error("[WatermarkRemove] FAL 备用链路失败:", fallbackError);
|
||||
}
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: "创建去水印任务失败" },
|
||||
{ error: "去水印服务暂时不可用,请稍后重试" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
@ -448,10 +540,7 @@ export async function POST(request: NextRequest) {
|
||||
userId: session.user.id,
|
||||
startedAt: startTime,
|
||||
apiKey: config.apiKey,
|
||||
sourceImageUrls:
|
||||
typeof image_url === "string" && /^https?:\/\//i.test(image_url)
|
||||
? [image_url]
|
||||
: undefined,
|
||||
sourceImageUrls,
|
||||
});
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
|
||||
@ -18,6 +18,16 @@ type ExtendConfig = {
|
||||
|
||||
const VIP98K_EXPANSION_FALLBACK_MODEL = "google/nano-banana-pro/edit";
|
||||
const VIP98K_SOURCE_DOWNLOAD_TIMEOUT_MS = 120_000;
|
||||
const DASHSCOPE_MIN_IMAGE_EDGE = 512;
|
||||
const DASHSCOPE_MAX_IMAGE_EDGE = 4096;
|
||||
const DASHSCOPE_MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||
const DASHSCOPE_SUPPORTED_IMAGE_FORMATS = new Set([
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"heif",
|
||||
"webp",
|
||||
]);
|
||||
|
||||
export interface ExecuteExtendJobParams {
|
||||
userId: string;
|
||||
@ -126,6 +136,105 @@ async function uploadImageToS3ForAPI(imageBase64: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function normalizeDashscopeExpansionInput(imageUrl: string): Promise<string> {
|
||||
const response = await fetch(imageUrl, {
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
signal: AbortSignal.timeout(VIP98K_SOURCE_DOWNLOAD_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new ExtendServiceError(
|
||||
"下载待拓展图片失败",
|
||||
502,
|
||||
`${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const sourceBuffer = Buffer.from(await response.arrayBuffer());
|
||||
const sharp = await import("sharp");
|
||||
const image = sharp.default(sourceBuffer, { failOn: "none" });
|
||||
const metadata = await image.metadata();
|
||||
const width = Number(metadata.width || 0);
|
||||
const height = Number(metadata.height || 0);
|
||||
const format = String(metadata.format || "").toLowerCase();
|
||||
if (!width || !height) {
|
||||
throw new ExtendServiceError("无法识别待拓展图片尺寸", 422);
|
||||
}
|
||||
|
||||
const withinEdgeLimits =
|
||||
width >= DASHSCOPE_MIN_IMAGE_EDGE &&
|
||||
height >= DASHSCOPE_MIN_IMAGE_EDGE &&
|
||||
width <= DASHSCOPE_MAX_IMAGE_EDGE &&
|
||||
height <= DASHSCOPE_MAX_IMAGE_EDGE;
|
||||
const withinFileLimit = sourceBuffer.length <= DASHSCOPE_MAX_IMAGE_BYTES;
|
||||
if (
|
||||
withinEdgeLimits &&
|
||||
withinFileLimit &&
|
||||
DASHSCOPE_SUPPORTED_IMAGE_FORMATS.has(format)
|
||||
) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
const downscale = Math.min(
|
||||
1,
|
||||
DASHSCOPE_MAX_IMAGE_EDGE / width,
|
||||
DASHSCOPE_MAX_IMAGE_EDGE / height,
|
||||
);
|
||||
let targetWidth = Math.max(1, Math.round(width * downscale));
|
||||
let targetHeight = Math.max(1, Math.round(height * downscale));
|
||||
if (Math.min(targetWidth, targetHeight) < DASHSCOPE_MIN_IMAGE_EDGE) {
|
||||
const upscale =
|
||||
DASHSCOPE_MIN_IMAGE_EDGE / Math.min(targetWidth, targetHeight);
|
||||
targetWidth = Math.max(1, Math.round(targetWidth * upscale));
|
||||
targetHeight = Math.max(1, Math.round(targetHeight * upscale));
|
||||
}
|
||||
|
||||
const resizeScale = Math.min(
|
||||
1,
|
||||
DASHSCOPE_MAX_IMAGE_EDGE / targetWidth,
|
||||
DASHSCOPE_MAX_IMAGE_EDGE / targetHeight,
|
||||
);
|
||||
targetWidth = Math.max(1, Math.round(targetWidth * resizeScale));
|
||||
targetHeight = Math.max(1, Math.round(targetHeight * resizeScale));
|
||||
|
||||
const canvasWidth = Math.max(DASHSCOPE_MIN_IMAGE_EDGE, targetWidth);
|
||||
const canvasHeight = Math.max(DASHSCOPE_MIN_IMAGE_EDGE, targetHeight);
|
||||
const left = Math.floor((canvasWidth - targetWidth) / 2);
|
||||
const top = Math.floor((canvasHeight - targetHeight) / 2);
|
||||
const normalizedBuffer = await image
|
||||
.rotate()
|
||||
.resize(targetWidth, targetHeight, { fit: "fill" })
|
||||
.flatten({ background: "#ffffff" })
|
||||
.extend({
|
||||
left,
|
||||
right: canvasWidth - targetWidth - left,
|
||||
top,
|
||||
bottom: canvasHeight - targetHeight - top,
|
||||
background: "#ffffff",
|
||||
})
|
||||
.jpeg({ quality: 88, mozjpeg: true })
|
||||
.toBuffer();
|
||||
|
||||
const normalizedArrayBuffer = normalizedBuffer.buffer.slice(
|
||||
normalizedBuffer.byteOffset,
|
||||
normalizedBuffer.byteOffset + normalizedBuffer.byteLength,
|
||||
) as ArrayBuffer;
|
||||
const uploaded = await uploadFile(
|
||||
`extend-dashscope-input-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`,
|
||||
normalizedArrayBuffer,
|
||||
);
|
||||
console.info("[Extend Service] Normalized DashScope input image", {
|
||||
sourceWidth: width,
|
||||
sourceHeight: height,
|
||||
sourceBytes: sourceBuffer.length,
|
||||
sourceFormat: format,
|
||||
targetWidth: canvasWidth,
|
||||
targetHeight: canvasHeight,
|
||||
targetBytes: normalizedBuffer.length,
|
||||
});
|
||||
return uploaded.url;
|
||||
}
|
||||
|
||||
function isTemporaryVip98kExpansionError(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error || "");
|
||||
return /HTTP\s+(?:429|5\d\d)|temporar(?:y|ily)|timeout|timed out|unavailable/i.test(
|
||||
@ -365,8 +474,17 @@ export async function executeExtendJob(
|
||||
throw new ExtendServiceError("无法获取输入图片URL", 400);
|
||||
}
|
||||
|
||||
const preparedInputImageUrl =
|
||||
config.provider === "vip98k"
|
||||
? inputImageUrl
|
||||
: await normalizeDashscopeExpansionInput(inputImageUrl);
|
||||
const startTime = Date.now();
|
||||
const extendedImageUrl = await extendImage(inputImageUrl, config, xScale, yScale);
|
||||
const extendedImageUrl = await extendImage(
|
||||
preparedInputImageUrl,
|
||||
config,
|
||||
xScale,
|
||||
yScale,
|
||||
);
|
||||
const imageResponse = await fetch(extendedImageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new ExtendServiceError(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user