fix: parse text edit image results robustly
This commit is contained in:
parent
a15a26ba99
commit
edcbddb0fa
@ -15,10 +15,13 @@ import { createRunway } from "@/lib/ai/providers/runway";
|
||||
import { createKrea } from "@/lib/ai/providers/krea";
|
||||
import { createVolcanoVision } from "@/lib/ai/providers/volcano-vision";
|
||||
import { isImageGenerationModelDescriptor } from "@/lib/ai/model-classification";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 120;
|
||||
|
||||
const logger = createLogger("canvas-text-edit");
|
||||
|
||||
const textItemSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
originalText: z.string().min(1),
|
||||
@ -115,20 +118,118 @@ function createTextEditProvider(
|
||||
}
|
||||
}
|
||||
|
||||
function parseImageFromMarkdown(markdown: string): { originalUrl: string; thumbnailUrl: string } {
|
||||
function normalizeGeneratedImageUrl(value: unknown) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return "";
|
||||
return raw
|
||||
.replace(/^["'([{<]+/, "")
|
||||
.replace(/[)"'\]},;。]+$/, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isHttpImageUrl(value: unknown) {
|
||||
const raw = normalizeGeneratedImageUrl(value);
|
||||
if (!raw || !/^https?:\/\//i.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function collectImageUrlsFromUnknown(value: unknown, output: Set<string>) {
|
||||
if (value === null || value === undefined) return;
|
||||
|
||||
if (typeof value === "string") {
|
||||
const raw = normalizeGeneratedImageUrl(value);
|
||||
if (isHttpImageUrl(raw)) {
|
||||
output.add(raw);
|
||||
return;
|
||||
}
|
||||
const matches = raw.match(/https?:\/\/[^\s"'<>),\]}]+/gi) || [];
|
||||
for (const match of matches) {
|
||||
const normalized = normalizeGeneratedImageUrl(match);
|
||||
if (isHttpImageUrl(normalized)) {
|
||||
output.add(normalized);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => collectImageUrlsFromUnknown(item, output));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
Object.entries(value as Record<string, unknown>).forEach(([key, entryValue]) => {
|
||||
if (
|
||||
/(url|uri|image|thumbnail|original|output|result|file)/i.test(key) ||
|
||||
typeof entryValue === "object"
|
||||
) {
|
||||
collectImageUrlsFromUnknown(entryValue, output);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function pickGeneratedImageUrl(urls: Iterable<string>) {
|
||||
const candidates = Array.from(new Set(Array.from(urls).map(normalizeGeneratedImageUrl)))
|
||||
.filter(isHttpImageUrl);
|
||||
const imageLike = candidates.find((url) =>
|
||||
/\.(avif|gif|jpe?g|png|webp)(?:[?#]|$)/i.test(url)
|
||||
);
|
||||
return imageLike || candidates[0] || "";
|
||||
}
|
||||
|
||||
function parseImageFromGenerationText(text: string): { originalUrl: string; thumbnailUrl: string } {
|
||||
const markdown = String(text || "").trim();
|
||||
const strictMatch = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/);
|
||||
if (strictMatch) {
|
||||
const originalUrl = normalizeGeneratedImageUrl(strictMatch[1]);
|
||||
const thumbnailUrl = normalizeGeneratedImageUrl(strictMatch[2]);
|
||||
if (isHttpImageUrl(originalUrl)) {
|
||||
return {
|
||||
originalUrl,
|
||||
thumbnailUrl: isHttpImageUrl(thumbnailUrl) ? thumbnailUrl : originalUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const markdownUrls = new Set<string>();
|
||||
const markdownImagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
|
||||
let markdownMatch: RegExpExecArray | null;
|
||||
while ((markdownMatch = markdownImagePattern.exec(markdown)) !== null) {
|
||||
const url = normalizeGeneratedImageUrl(markdownMatch[1]);
|
||||
if (isHttpImageUrl(url)) {
|
||||
markdownUrls.add(url);
|
||||
}
|
||||
}
|
||||
|
||||
const markdownUrl = pickGeneratedImageUrl(markdownUrls);
|
||||
if (markdownUrl) {
|
||||
return {
|
||||
originalUrl: strictMatch[1],
|
||||
thumbnailUrl: strictMatch[2],
|
||||
originalUrl: markdownUrl,
|
||||
thumbnailUrl: markdownUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const simpleMatch = markdown.match(/!\[[^\]]*\]\(([^)]+)\)/);
|
||||
if (simpleMatch) {
|
||||
const collectedUrls = new Set<string>();
|
||||
try {
|
||||
collectImageUrlsFromUnknown(JSON.parse(markdown), collectedUrls);
|
||||
} catch {
|
||||
collectImageUrlsFromUnknown(markdown, collectedUrls);
|
||||
}
|
||||
|
||||
const url = pickGeneratedImageUrl(collectedUrls);
|
||||
if (url) {
|
||||
return {
|
||||
originalUrl: simpleMatch[1],
|
||||
thumbnailUrl: simpleMatch[1],
|
||||
originalUrl: url,
|
||||
thumbnailUrl: url,
|
||||
};
|
||||
}
|
||||
|
||||
@ -238,7 +339,7 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
console.info("[Canvas Text Edit] selected model:", {
|
||||
logger.info("selected_model", {
|
||||
providerType: selected.provider.type,
|
||||
providerName: selected.provider.name,
|
||||
modelId: selected.model.modelId,
|
||||
@ -270,7 +371,7 @@ export async function POST(request: Request) {
|
||||
throw new Error("模型未返回结果");
|
||||
}
|
||||
|
||||
const { originalUrl, thumbnailUrl } = parseImageFromMarkdown(result.text);
|
||||
const { originalUrl, thumbnailUrl } = parseImageFromGenerationText(result.text);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@ -281,7 +382,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Canvas Text Edit] Error:", error);
|
||||
logger.error("request_failed", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user