199 lines
6.0 KiB
JavaScript
199 lines
6.0 KiB
JavaScript
/**
|
||
* Vision产品特征提取器
|
||
* 支持超时重试 + 缓存
|
||
*/
|
||
|
||
const axios = require('axios');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||
|
||
// Vision提取Prompt
|
||
const VISION_EXTRACT_PROMPT = `Analyze this pet product image in EXTREME detail.
|
||
|
||
Output ONLY a valid JSON object (no markdown, no explanation, no thinking):
|
||
|
||
{
|
||
"color": {
|
||
"primary": "<hex code like #C3E6E8>",
|
||
"name": "<descriptive name like 'ice blue', 'mint green'>",
|
||
"secondary": "<accent colors>"
|
||
},
|
||
"shape": {
|
||
"type": "<flower/fan/cone/donut>",
|
||
"petal_count": <number of segments>,
|
||
"opening": "<C-shaped/full-circle/adjustable>",
|
||
"description": "<detailed shape description>"
|
||
},
|
||
"material": {
|
||
"type": "<PU/nylon/polyester/fabric>",
|
||
"finish": "<glossy/matte/satin>",
|
||
"texture": "<smooth/quilted/padded>"
|
||
},
|
||
"edge_binding": {
|
||
"color": "<color of inner neck edge>",
|
||
"material": "<ribbed elastic/fabric>"
|
||
},
|
||
"closure": {
|
||
"type": "<velcro/button/snap>",
|
||
"color": "<white/matching>",
|
||
"position": "<location description>"
|
||
},
|
||
"logo": {
|
||
"text": "<brand name>",
|
||
"style": "<embroidered/printed/tag>",
|
||
"position": "<location>"
|
||
},
|
||
"unique_features": ["<list distinctive features>"],
|
||
"overall_description": "<2-3 sentence summary for image generation>"
|
||
}`;
|
||
|
||
/**
|
||
* 调用Vision API提取产品特征
|
||
* @param {string} imageUrl - 图片URL
|
||
* @param {object} options - 配置选项
|
||
* @returns {object|null} - 提取的产品特征JSON
|
||
*/
|
||
async function extractProductFeatures(imageUrl, options = {}) {
|
||
const {
|
||
maxRetries = 3,
|
||
timeout = 120000, // 120秒
|
||
retryDelay = 5000,
|
||
cacheDir = null,
|
||
cacheKey = null
|
||
} = options;
|
||
|
||
// 检查缓存
|
||
if (cacheDir && cacheKey) {
|
||
const cachePath = path.join(cacheDir, `vision-cache-${cacheKey}.json`);
|
||
if (fs.existsSync(cachePath)) {
|
||
try {
|
||
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||
if (cached && cached.color) {
|
||
console.log(' 📦 使用缓存的Vision结果');
|
||
return cached;
|
||
}
|
||
} catch (e) {
|
||
// 缓存无效,继续请求
|
||
}
|
||
}
|
||
}
|
||
|
||
let lastError = null;
|
||
|
||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
console.log(` 🔍 Vision分析 (尝试 ${attempt}/${maxRetries})...`);
|
||
|
||
try {
|
||
const response = await axios.post(`${API_BASE}/chat/index`, {
|
||
key: API_KEY,
|
||
model: 'gemini-3-pro',
|
||
content: VISION_EXTRACT_PROMPT,
|
||
image_url: imageUrl
|
||
}, { timeout });
|
||
|
||
const content = response.data.data?.choices?.[0]?.message?.content ||
|
||
response.data.data?.content;
|
||
|
||
if (!content) {
|
||
throw new Error('Vision响应为空');
|
||
}
|
||
|
||
// 提取JSON(跳过thinking部分)
|
||
let jsonStr = content;
|
||
|
||
// 如果有<think>标签,跳过它
|
||
const thinkEnd = content.indexOf('</think>');
|
||
if (thinkEnd !== -1) {
|
||
jsonStr = content.substring(thinkEnd + 8);
|
||
}
|
||
|
||
// 提取JSON
|
||
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
const parsed = JSON.parse(jsonMatch[0]);
|
||
|
||
// 验证必要字段
|
||
if (parsed.color && parsed.shape) {
|
||
console.log(' ✓ Vision提取成功');
|
||
|
||
// 保存缓存
|
||
if (cacheDir && cacheKey) {
|
||
const cachePath = path.join(cacheDir, `vision-cache-${cacheKey}.json`);
|
||
fs.writeFileSync(cachePath, JSON.stringify(parsed, null, 2));
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
}
|
||
|
||
throw new Error('无法解析有效的JSON');
|
||
|
||
} catch (error) {
|
||
lastError = error;
|
||
console.log(` ⚠️ 尝试 ${attempt} 失败: ${error.message}`);
|
||
|
||
if (attempt < maxRetries) {
|
||
console.log(` ⏳ ${retryDelay/1000}秒后重试...`);
|
||
await new Promise(r => setTimeout(r, retryDelay));
|
||
}
|
||
}
|
||
}
|
||
|
||
console.error(` ❌ Vision提取最终失败: ${lastError?.message}`);
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 将Vision结果转换为Golden Description
|
||
*/
|
||
function buildGoldenDescription(visionResult, productType = 'pet recovery cone') {
|
||
if (!visionResult) {
|
||
return `
|
||
PRODUCT: ${productType}
|
||
- Shape: Soft flower/petal shape with C-shaped opening
|
||
- Material: Soft waterproof fabric
|
||
- Closure: Velcro strap
|
||
- Comfortable design for pets
|
||
|
||
CRITICAL: Follow the reference image EXACTLY for product shape and color.
|
||
`;
|
||
}
|
||
|
||
const r = visionResult;
|
||
|
||
return `
|
||
EXACT PRODUCT APPEARANCE (MUST MATCH REFERENCE IMAGE):
|
||
|
||
- Shape: ${r.shape?.petal_count || '7-8'}-PETAL ${(r.shape?.type || 'flower').toUpperCase()} shape
|
||
- Opening: ${r.shape?.opening || 'C-shaped'} (NOT a full circle)
|
||
- Color: ${(r.color?.name || 'ice blue').toUpperCase()} (${r.color?.primary || '#C3E6E8'})
|
||
- Material: ${r.material?.finish || 'Soft'} ${r.material?.type || 'waterproof fabric'} with ${r.material?.texture || 'padded'} texture
|
||
- Edge binding: ${r.edge_binding?.color || 'Matching color'} ${r.edge_binding?.material || 'ribbed elastic'} around inner neck hole
|
||
- Closure: ${r.closure?.color || 'White'} ${r.closure?.type || 'velcro'} on ${r.closure?.position || 'one segment'}
|
||
- Logo: "${r.logo?.text || 'TOUCHDOG'}" ${r.logo?.style || 'embroidered'} on ${r.logo?.position || 'one petal'}
|
||
|
||
UNIQUE FEATURES:
|
||
${(r.unique_features || ['Scalloped petal edges', 'Radial stitching', 'Soft padded construction']).map(f => `- ${f}`).join('\n')}
|
||
|
||
OVERALL: ${r.overall_description || 'A soft, comfortable pet recovery cone with flower-petal design.'}
|
||
|
||
CRITICAL PROHIBITIONS:
|
||
- ❌ NO printed colorful patterns (solid color only unless reference shows otherwise)
|
||
- ❌ NO hard plastic transparent cones
|
||
- ❌ NO fully circular/closed shapes (must match reference C-opening)
|
||
- ❌ NO random brand logos
|
||
- ❌ MUST match reference image product EXACTLY
|
||
`.trim();
|
||
}
|
||
|
||
module.exports = {
|
||
extractProductFeatures,
|
||
buildGoldenDescription,
|
||
VISION_EXTRACT_PROMPT
|
||
};
|
||
|
||
|