import "server-only"; import { generateText } from "ai"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { callQwen3Vl } from "@/lib/ai/qwen3-vl"; import { getEnabledAiModelsWithProvider } from "@/lib/db/queries"; const QWEN_VL_MODEL_MATCHER = /(qwen|千问|通义).*(vl|vision)|qwen3-vl|qwen-vl/i; function sanitizeAppearanceSummary(value: unknown, maxLength = 220) { return String(value || "").replace(/\s+/g, " ").trim().slice(0, maxLength); } function extractJsonObjectText(text: string): string | null { const normalized = String(text || "").trim(); if (!normalized) { return null; } const fencedMatch = normalized.match(/```(?:json)?\s*([\s\S]*?)```/i); if (fencedMatch?.[1]) { const fencedText = fencedMatch[1].trim(); if (fencedText.startsWith("{") && fencedText.endsWith("}")) { return fencedText; } } if (normalized.startsWith("{") && normalized.endsWith("}")) { return normalized; } const firstBraceIndex = normalized.indexOf("{"); const lastBraceIndex = normalized.lastIndexOf("}"); if (firstBraceIndex >= 0 && lastBraceIndex > firstBraceIndex) { return normalized.slice(firstBraceIndex, lastBraceIndex + 1); } return null; } function parseAppearanceSummary(text: string) { const jsonText = extractJsonObjectText(text); if (jsonText) { try { const parsed = JSON.parse(jsonText) as Record; const summary = sanitizeAppearanceSummary( parsed.appearanceSummary || parsed.summary || parsed.description ); if (summary) { return summary; } } catch (error) { console.warn( "[Workflow Material Appearance] Failed to parse JSON response:", error ); } } return sanitizeAppearanceSummary( String(text || "") .replace(/```(?:json)?/gi, "") .replace(/```/g, "") ); } function buildAppearancePrompt() { return [ "你是电商商品白底图识别器。", "请只基于商品本体,识别并描述产品的外观结构和颜色,用于后续生图时保持产品结构不要改变。", "描述重点:1. 产品品类与整体轮廓;2. 关键结构件及数量关系,例如层板、格口、门板、抽屉、腿、扶手、靠背、边框、台面、开孔、开口方向;3. 主色、副色与颜色分布。", "不要分析或描述材质、纹理、表面工艺、木纹、金属感、布艺感,也不要描述背景、光影、拍摄角度、水印、文案,更不要输出营销形容词。", '只输出 JSON:{"appearanceSummary":"..."}', "appearanceSummary 必须是中文,长度 60 到 120 字,具体、克制、可直接作为生图约束。", ].join("\n"); } async function analyzeWithConfiguredQwenVisionModel(params: { imageUrl: string; prompt: string; }) { const configuredModels = await getEnabledAiModelsWithProvider().catch((error) => { console.warn( "[Workflow Material Appearance] Failed to load configured models:", error ); return []; }); const configuredQwenVisionModel = configuredModels.find( ({ model }) => model.supportsVision && !model.isSubModel && QWEN_VL_MODEL_MATCHER.test(`${model.displayName} ${model.modelId}`) ); if (!configuredQwenVisionModel) { return null; } try { const languageModel = await getLanguageModelById( configuredQwenVisionModel.model.id ); if (!languageModel) { return null; } const { text } = await generateText({ model: languageModel, messages: [ { role: "user", content: [ { type: "text", text: params.prompt, }, { type: "image", image: params.imageUrl, }, ], }, ], temperature: 0, maxTokens: 220, abortSignal: AbortSignal.timeout(25_000), } as any); return parseAppearanceSummary(text); } catch (error) { console.warn( "[Workflow Material Appearance] Configured Qwen-VL analysis failed:", error ); return null; } } export async function analyzeWorkflowMaterialAppearanceFromImage(imageUrl: string) { const normalizedUrl = String(imageUrl || "").trim(); if (!/^https?:\/\//i.test(normalizedUrl)) { return null; } const prompt = buildAppearancePrompt(); const configuredSummary = await analyzeWithConfiguredQwenVisionModel({ imageUrl: normalizedUrl, prompt, }); if (configuredSummary) { return configuredSummary; } try { const result = await callQwen3Vl({ imageUrl: normalizedUrl, prompt, temperature: 0, maxTokens: 220, timeoutMs: 25_000, retries: 1, }); return parseAppearanceSummary(result.text); } catch (error) { console.warn( "[Workflow Material Appearance] Fallback Qwen3-VL analysis failed:", error ); return null; } }