Upload latest code and optimized prompts (v6)
This commit is contained in:
383
poc-workflow.js
Normal file
383
poc-workflow.js
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* POC Workflow: Vision Analysis -> Optimized Generation (Full 12 Images)
|
||||
* API Provider: wuyinkeji.com
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
|
||||
// 配置
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
const OUTPUT_DIR = path.join(__dirname, 'output_poc_full');
|
||||
const MATERIAL_DIR = path.join(__dirname, '素材/素材/已有的素材');
|
||||
|
||||
// R2客户端 (保持不变)
|
||||
const r2Client = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
async function uploadToR2(filePath) {
|
||||
const fileName = `poc-${Date.now()}-${path.basename(filePath)}`;
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
|
||||
await r2Client.send(new PutObjectCommand({
|
||||
Bucket: process.env.R2_BUCKET_NAME || 'ai-flow',
|
||||
Key: fileName,
|
||||
Body: fileBuffer,
|
||||
ContentType: filePath.endsWith('.png') ? 'image/png' : 'image/jpeg',
|
||||
}));
|
||||
|
||||
const publicUrl = process.env.R2_PUBLIC_DOMAIN
|
||||
? `${process.env.R2_PUBLIC_DOMAIN}/${fileName}`
|
||||
: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET_NAME}/${fileName}`;
|
||||
|
||||
return publicUrl;
|
||||
}
|
||||
|
||||
// 核心函数1:视觉分析 (Vision Agent)
|
||||
async function analyzeProductVisuals(refImageUrls) {
|
||||
console.log('👁️ 正在进行视觉分析 (Vision Agent)...');
|
||||
|
||||
const analysisPrompt = `
|
||||
You are a professional e-commerce product photographer.
|
||||
Analyze these reference images of a pet recovery cone collar.
|
||||
Extract visual characteristics:
|
||||
1. Material & Texture: (e.g. glossy PU, matte cotton)
|
||||
2. Structure (Flat): (e.g. Open C-shape, Fan shape)
|
||||
3. Structure (Worn): (e.g. Cone shape flared OUTWARDS/FORWARD)
|
||||
4. Key Details: (e.g. logo, binding color)
|
||||
|
||||
Output JSON: { "material": "...", "structure_flat": "...", "structure_worn": "...", "details": "..." }
|
||||
`;
|
||||
|
||||
try {
|
||||
// 构造Wuyin Vision请求
|
||||
// 注意:根据文档截图,/api/chat/index 支持 image_url 参数
|
||||
// 这里我们只传第一张图作为主要参考,或者尝试传多张如果API支持
|
||||
// 由于接口文档通常 image_url 是字符串,我们先传最具代表性的图(通常是第一张)
|
||||
// 如果需要多图分析,可能需要多次调用或拼接
|
||||
|
||||
// 这里简单起见,我们取一张最清晰的图(假设是第一张或摊平图)进行分析
|
||||
const mainRefImage = refImageUrls.find(url => url.includes('5683')) || refImageUrls[0];
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_BASE}/chat/index`,
|
||||
{
|
||||
key: API_KEY,
|
||||
model: 'gemini-3-pro', // 明确指定 vision 模型为 gemini-3-pro
|
||||
content: analysisPrompt,
|
||||
image_url: mainRefImage
|
||||
},
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
|
||||
let content = response.data.data?.content || response.data.choices?.[0]?.message?.content;
|
||||
|
||||
if (!content) throw new Error('No content in vision response');
|
||||
|
||||
content = content.replace(/```json|```/g, '').trim();
|
||||
|
||||
// 尝试解析JSON,如果失败则用正则提取或兜底
|
||||
let analysis;
|
||||
try {
|
||||
analysis = JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.warn('JSON parse failed, using raw content');
|
||||
analysis = {
|
||||
material: "Smooth waterproof PU, slightly glossy",
|
||||
structure_flat: "Open C-shape/Fan shape",
|
||||
structure_worn: "Cone flared upwards/outwards",
|
||||
details: "Embroidered Touchdog logo"
|
||||
};
|
||||
}
|
||||
|
||||
console.log('✅ 视觉分析完成:', analysis);
|
||||
return analysis;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 视觉分析失败:', error.message);
|
||||
// 兜底描述
|
||||
return {
|
||||
material: "Smooth waterproof PU fabric with a slight sheen",
|
||||
structure_flat: "Open C-shape (Fan-like) with petal segments",
|
||||
structure_worn: "Cone shape flared UPWARDS/FORWARD around head",
|
||||
details: "Embroidered Touchdog logo on right petal"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 生图任务提交
|
||||
async function submitImageTask(prompt, refImageUrls, aspectRatio = '1:1') {
|
||||
try {
|
||||
// Wuyin API: /api/img/nanoBanana-pro
|
||||
// 参数: key, prompt, img_url (参考图), aspectRatio
|
||||
|
||||
// 注意:img_url 只能传一张。我们传最相关的一张。
|
||||
// 对于场景图,传佩戴图;对于平铺图,传平铺图。
|
||||
let refImg = refImageUrls[0];
|
||||
if (prompt.includes('flat') && refImageUrls.some(u => u.includes('5683'))) {
|
||||
refImg = refImageUrls.find(u => u.includes('5683')); // 摊平图
|
||||
} else if (refImageUrls.some(u => u.includes('6514'))) {
|
||||
refImg = refImageUrls.find(u => u.includes('6514')); // 佩戴图
|
||||
}
|
||||
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
img_url: refImg, // 传单张参考图强约束
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
console.log(` 提交任务... (Ref: ${path.basename(refImg || '')})`);
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload);
|
||||
|
||||
// 假设返回格式 { code: 200, data: { id: "..." }, msg: "success" }
|
||||
// 或者直接返回 task_id,需根据实际响应调整
|
||||
// 根据截图: 异步任务通常返回 id
|
||||
|
||||
// 这里假设 API 返回包含 task id
|
||||
const taskId = response.data.data?.id; // 修正: 获取 data.id
|
||||
|
||||
if (!taskId) {
|
||||
console.error('Submit response:', response.data);
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
|
||||
return taskId;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Submit failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询图片结果
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 60 * 2s = 2 mins
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId }
|
||||
});
|
||||
|
||||
// 状态判断修正: status 2 是成功
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && (data.status === 3 || data.status === 'failed')) { // 假设 3 是失败,或者保留 failed 字符串判断
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
console.log(` 状态: ${data.status} (等待中...)`);
|
||||
|
||||
// Still processing
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
process.stdout.write('.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Poll error:', error.message);
|
||||
throw error; // 直接抛出,触发重试
|
||||
}
|
||||
}
|
||||
throw new Error('Timeout waiting for image');
|
||||
}
|
||||
|
||||
// 生成单张图(含重试)
|
||||
async function generateSingleImage(item, refImageUrls) {
|
||||
console.log(`\n🎨 生成 ${item.id} (${item.type})...`);
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
if (retryCount > 0) console.log(` 重试第 ${retryCount} 次...`);
|
||||
|
||||
const taskId = await submitImageTask(item.prompt, refImageUrls, item.aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log(' ✓ 生成成功');
|
||||
|
||||
// 下载保存
|
||||
const imgRes = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, `${item.id}.jpg`), imgRes.data);
|
||||
console.log(' ✓ 保存本地');
|
||||
return; // 成功退出
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ✗ 失败: ${error.message}`);
|
||||
retryCount++;
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
console.error(` ❌ ${item.id} 最终失败`);
|
||||
}
|
||||
|
||||
// 主流程
|
||||
async function main() {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
// 1. 上传
|
||||
console.log('📤 Step 1: 上传素材...');
|
||||
const files = fs.readdirSync(MATERIAL_DIR).filter(f => /\.(jpg|png)$/i.test(f));
|
||||
const refImageUrls = [];
|
||||
files.sort();
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('.')) continue;
|
||||
const url = await uploadToR2(path.join(MATERIAL_DIR, file));
|
||||
refImageUrls.push(url);
|
||||
}
|
||||
console.log(` 共 ${refImageUrls.length} 张`);
|
||||
|
||||
// 2. 视觉分析
|
||||
const va = await analyzeProductVisuals(refImageUrls);
|
||||
// const va = {
|
||||
// material: "Smooth waterproof PU fabric with a slight glossy sheen",
|
||||
// structure_flat: "Open C-shape (Fan-like) with petal segments, NOT a closed circle",
|
||||
// structure_worn: "Cone shape flared UPWARDS/FORWARD around head (megaphone shape)",
|
||||
// details: "Embroidered Touchdog logo on right petal, teal edge binding"
|
||||
// };
|
||||
|
||||
// 3. 定义12张图的Prompt (结合视觉分析)
|
||||
const prompts = [
|
||||
// --- 主图 (1:1) ---
|
||||
{
|
||||
id: 'Main_01_Hero',
|
||||
type: '场景首图',
|
||||
aspectRatio: '1:1',
|
||||
prompt: `Amazon main image. Ragdoll cat wearing ${va.material} cone collar.
|
||||
CRITICAL: Cone opening must face FORWARD/OUTWARDS around the cat's face (like a megaphone), NOT wrapped like a scarf.
|
||||
Cat is looking comfortable. Warm cozy home background.
|
||||
Product texture: ${va.material}. Structure: ${va.structure_worn}.
|
||||
Logo: Touchdog embroidered on right side. High quality, 8k.`
|
||||
},
|
||||
{
|
||||
id: 'Main_02_Flat',
|
||||
type: '平铺图',
|
||||
aspectRatio: '1:1',
|
||||
prompt: `Product flat lay, white background.
|
||||
Structure: ${va.structure_flat} (Open C-shape/Fan-like, NOT closed circle).
|
||||
Texture: ${va.material}, glossy PU sheen.
|
||||
Details: Velcro strap visible, ${va.details}. Clean studio light.`
|
||||
},
|
||||
{
|
||||
id: 'Main_03_Function',
|
||||
type: '功能演示',
|
||||
aspectRatio: '1:1',
|
||||
prompt: `Product feature shot. Close-up of the velcro strap adjustment.
|
||||
Hands adjusting the strap for secure fit.
|
||||
Show the ${va.material} texture and stitching quality.
|
||||
Light blue background with text overlay "ADJUSTABLE FIT" (optional).`
|
||||
},
|
||||
{
|
||||
id: 'Main_04_Scenarios',
|
||||
type: '场景拼图',
|
||||
aspectRatio: '1:1',
|
||||
prompt: `Split screen composition.
|
||||
Top: Cat eating food easily while wearing the cone (cone facing forward).
|
||||
Bottom: Cat sleeping comfortably.
|
||||
Show flexibility and comfort. Soft lighting.`
|
||||
},
|
||||
{
|
||||
id: 'Main_05_Size',
|
||||
type: '尺寸图',
|
||||
aspectRatio: '1:1',
|
||||
prompt: `Product infographic. The ${va.structure_flat} cone laid flat.
|
||||
Ruler graphics measuring the depth (24.5cm).
|
||||
Size chart table on the side: XS, S, M, L, XL.
|
||||
Professional clean design.`
|
||||
},
|
||||
{
|
||||
id: 'Main_06_Brand',
|
||||
type: '品牌汇总',
|
||||
aspectRatio: '1:1',
|
||||
prompt: `Brand image. Large Touchdog logo in center.
|
||||
Product floating in background.
|
||||
Icons: Feather (Lightweight), Water drop (Waterproof), Cotton (Soft).
|
||||
High-end branding style.`
|
||||
},
|
||||
// --- A+图 (3:2) ---
|
||||
{
|
||||
id: 'APlus_01_Banner',
|
||||
type: '品牌Banner',
|
||||
aspectRatio: '3:2',
|
||||
prompt: `Amazon A+ Banner. Wide shot of modern living room.
|
||||
White cat wearing the ${va.material} cone walking on rug.
|
||||
Cone flares outwards correctly.
|
||||
Text space on left. Warm, inviting atmosphere.`
|
||||
},
|
||||
{
|
||||
id: 'APlus_02_Comparison',
|
||||
type: '竞品对比',
|
||||
aspectRatio: '3:2',
|
||||
prompt: `Comparison image. Split screen.
|
||||
Left (Touchdog): Colorful, happy cat wearing soft blue cone. Green Checkmark.
|
||||
Right (Others): Black and white, sad cat wearing hard plastic transparent cone. Red Cross.
|
||||
Text: "Soft & Comfy" vs "Hard & Heavy".`
|
||||
},
|
||||
{
|
||||
id: 'APlus_03_Detail',
|
||||
type: '材质细节',
|
||||
aspectRatio: '3:2',
|
||||
prompt: `Extreme close-up macro shot of the product material.
|
||||
Show the waterproof PU texture with water droplets beading off.
|
||||
Show the neat stitching and soft edge binding.
|
||||
High tech, quality feel.`
|
||||
},
|
||||
{
|
||||
id: 'APlus_04_Waterproof',
|
||||
type: '防水测试',
|
||||
aspectRatio: '3:2',
|
||||
prompt: `Action shot. Water being poured onto the blue cone collar.
|
||||
Water repelling instantly, rolling off the surface.
|
||||
Demonstrates "Easy to Wipe" feature.
|
||||
Bright lighting.`
|
||||
},
|
||||
{
|
||||
id: 'APlus_05_Storage',
|
||||
type: '收纳展示',
|
||||
aspectRatio: '3:2',
|
||||
prompt: `Lifestyle shot. The cone collar folded/rolled up compactly.
|
||||
Placed inside a small travel bag or drawer.
|
||||
Shows "Easy Storage" feature.
|
||||
Clean composition.`
|
||||
},
|
||||
{
|
||||
id: 'APlus_06_Guide',
|
||||
type: '选购指南',
|
||||
aspectRatio: '3:2',
|
||||
prompt: `Sizing guide banner.
|
||||
Illustration of how to measure cat's neck circumference.
|
||||
Touchdog branding elements.
|
||||
Clean, instructional design.`
|
||||
}
|
||||
];
|
||||
|
||||
// 4. 执行生成
|
||||
console.log('\n🚀 开始全量生成 (12张)...');
|
||||
for (const item of prompts) {
|
||||
await generateSingleImage(item, refImageUrls);
|
||||
}
|
||||
|
||||
console.log('\n✅ POC 完整验证结束。');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user